From 215d53e0214ea9cbc9457db5334cfa9b2cec3ebd Mon Sep 17 00:00:00 2001 From: Nic Gibson Date: Tue, 16 Apr 2024 12:13:13 +0100 Subject: [PATCH 001/230] Ensure that JSON.GET returns Nil response Updated JSONCmd.readReply to return redis.Nil if no results Added a doc line for Val() and Expanded() methods of JSONCmd Added a test case for non existent keys in json_test.go --- json.go | 12 +++++++++--- json_test.go | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/json.go b/json.go index ca731db3a7..94254d914e 100644 --- a/json.go +++ b/json.go @@ -82,6 +82,7 @@ func (cmd *JSONCmd) SetVal(val string) { cmd.val = val } +// Val returns the result of the JSON.GET command as a string. func (cmd *JSONCmd) Val() string { if len(cmd.val) == 0 && cmd.expanded != nil { val, err := json.Marshal(cmd.expanded) @@ -100,6 +101,7 @@ func (cmd *JSONCmd) Result() (string, error) { return cmd.Val(), cmd.Err() } +// Expanded returns the result of the JSON.GET command as unmarshalled JSON. func (cmd JSONCmd) Expanded() (interface{}, error) { if len(cmd.val) != 0 && cmd.expanded == nil { err := json.Unmarshal([]byte(cmd.val), &cmd.expanded) @@ -112,10 +114,10 @@ func (cmd JSONCmd) Expanded() (interface{}, error) { } func (cmd *JSONCmd) readReply(rd *proto.Reader) error { - // nil response from JSON.(M)GET (cmd.baseCmd.err will be "redis: nil") - if cmd.baseCmd.Err() == Nil { + + if cmd.baseCmd.Err() != nil { cmd.val = "" - return Nil + return cmd.baseCmd.Err() } if readType, err := rd.PeekReplyType(); err != nil { @@ -126,6 +128,9 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { if err != nil { return err } + if size == 0 { + return Nil + } expanded := make([]interface{}, size) @@ -141,6 +146,7 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { return err } else if str == "" || err == Nil { cmd.val = "" + return Nil } else { cmd.val = str } diff --git a/json_test.go b/json_test.go index 4e9718a4e0..89b4b09955 100644 --- a/json_test.go +++ b/json_test.go @@ -123,9 +123,14 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(err).NotTo(HaveOccurred()) Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]")) + _, err = client.JSONGet(ctx, "this-key-does-not-exist", "$").Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(BeIdenticalTo(redis.Nil)) + resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() Expect(err).NotTo(HaveOccurred()) Expect(resArr).To(Equal([]int64{1, 2})) + }) It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { From 603b0ba7a38d9186921540170d158b644652fc58 Mon Sep 17 00:00:00 2001 From: deferdeter <166976362+deferdeter@users.noreply.github.com> Date: Wed, 17 Apr 2024 00:02:39 +0800 Subject: [PATCH 002/230] Fix typo in comment (#2972) Signed-off-by: deferdeter --- example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index 62aa8cb56a..28d14b65aa 100644 --- a/example_test.go +++ b/example_test.go @@ -482,7 +482,7 @@ func ExampleClient_Watch() { return err } - // Actual opperation (local in optimistic lock). + // Actual operation (local in optimistic lock). n++ // Operation is committed only if the watched keys remain unchanged. From b64b22e73e1d2fa8c877b7d17ed992d4e1304cd9 Mon Sep 17 00:00:00 2001 From: Akash Darshan Date: Sun, 21 Apr 2024 22:52:00 +0530 Subject: [PATCH 003/230] Adding BitfieldRo in BitMapCmdable interface (#2962) Co-authored-by: Monkey --- bitmap_commands.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bitmap_commands.go b/bitmap_commands.go index 9cd808995f..a215582890 100644 --- a/bitmap_commands.go +++ b/bitmap_commands.go @@ -16,6 +16,7 @@ type BitMapCmdable interface { BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd + BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd } func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd { From 8f17cbab9e1198ad8d275d3d43f29da9ef5d0063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Bal=C3=A1=C5=BE?= Date: Sat, 27 Apr 2024 08:42:30 +0200 Subject: [PATCH 004/230] Fix XGroup first pos key (#2983) --- stream_commands.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stream_commands.go b/stream_commands.go index 0a9869202a..1ad33740ce 100644 --- a/stream_commands.go +++ b/stream_commands.go @@ -178,36 +178,42 @@ func (c cmdable) XReadStreams(ctx context.Context, streams ...string) *XStreamSl func (c cmdable) XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd { cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start) + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } func (c cmdable) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd { cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start, "mkstream") + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } func (c cmdable) XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd { cmd := NewStatusCmd(ctx, "xgroup", "setid", stream, group, start) + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } func (c cmdable) XGroupDestroy(ctx context.Context, stream, group string) *IntCmd { cmd := NewIntCmd(ctx, "xgroup", "destroy", stream, group) + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } func (c cmdable) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd { cmd := NewIntCmd(ctx, "xgroup", "createconsumer", stream, group, consumer) + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } func (c cmdable) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd { cmd := NewIntCmd(ctx, "xgroup", "delconsumer", stream, group, consumer) + cmd.SetFirstKeyPos(2) _ = c(ctx, cmd) return cmd } From 73600901563de6bdd3ad8ea61d6d6a048a4abecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Bal=C3=A1=C5=BE?= Date: Sun, 28 Apr 2024 06:37:44 +0200 Subject: [PATCH 005/230] Handle IPv6 in isMovedError (#2981) * Handle IPv6 in isMovedError * Simplify GetAddr --------- Co-authored-by: Monkey --- error.go | 3 +++ internal/util.go | 17 +++++++++++++++++ internal/util_test.go | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/error.go b/error.go index 8a59913be8..9b348193a4 100644 --- a/error.go +++ b/error.go @@ -7,6 +7,7 @@ import ( "net" "strings" + "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" ) @@ -129,7 +130,9 @@ func isMovedError(err error) (moved bool, ask bool, addr string) { if ind == -1 { return false, false, "" } + addr = s[ind+1:] + addr = internal.GetAddr(addr) return } diff --git a/internal/util.go b/internal/util.go index ed81ad7aa1..235a91afa9 100644 --- a/internal/util.go +++ b/internal/util.go @@ -2,6 +2,7 @@ package internal import ( "context" + "net" "strings" "time" @@ -64,3 +65,19 @@ func ReplaceSpaces(s string) string { return builder.String() } + +func GetAddr(addr string) string { + ind := strings.LastIndexByte(addr, ':') + if ind == -1 { + return "" + } + + if strings.IndexByte(addr, '.') != -1 { + return addr + } + + if addr[0] == '[' { + return addr + } + return net.JoinHostPort(addr[:ind], addr[ind+1:]) +} diff --git a/internal/util_test.go b/internal/util_test.go index f090ebaa4b..57f7f9fa15 100644 --- a/internal/util_test.go +++ b/internal/util_test.go @@ -51,3 +51,24 @@ func TestIsLower(t *testing.T) { Expect(isLower(str)).To(BeTrue()) }) } + +func TestGetAddr(t *testing.T) { + It("getAddr", func() { + str := "127.0.0.1:1234" + Expect(GetAddr(str)).To(Equal(str)) + + str = "[::1]:1234" + Expect(GetAddr(str)).To(Equal(str)) + + str = "[fd01:abcd::7d03]:6379" + Expect(GetAddr(str)).To(Equal(str)) + + Expect(GetAddr("::1:1234")).To(Equal("[::1]:1234")) + + Expect(GetAddr("fd01:abcd::7d03:6379")).To(Equal("[fd01:abcd::7d03]:6379")) + + Expect(GetAddr("127.0.0.1")).To(Equal("")) + + Expect(GetAddr("127")).To(Equal("")) + }) +} From d453af549f588ccd237eef5eb4203cb94febbf2c Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Thu, 9 May 2024 23:52:32 -0700 Subject: [PATCH 006/230] Remove skipping span creation by checking parent spans (#2980) * Remove skipping span creation by checking parent spans * Update CHANGELOG --- CHANGELOG.md | 9 +++++++++ extra/redisotel/tracing.go | 12 ------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297438a9fc..e1652b179a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +### Changed + +* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) + Users can use the OpenTelemetry sampler to control the sampling behavior. + For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep + a similar behavior (drop orphan spans) of `go-redis` as before. + ## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) diff --git a/extra/redisotel/tracing.go b/extra/redisotel/tracing.go index 2c6c1b0b5a..3d5f3426ca 100644 --- a/extra/redisotel/tracing.go +++ b/extra/redisotel/tracing.go @@ -91,10 +91,6 @@ func newTracingHook(connString string, opts ...TracingOption) *tracingHook { func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook { return func(ctx context.Context, network, addr string) (net.Conn, error) { - if !trace.SpanFromContext(ctx).IsRecording() { - return hook(ctx, network, addr) - } - ctx, span := th.conf.tracer.Start(ctx, "redis.dial", th.spanOpts...) defer span.End() @@ -109,10 +105,6 @@ func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook { func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - if !trace.SpanFromContext(ctx).IsRecording() { - return hook(ctx, cmd) - } - fn, file, line := funcFileLine("github.com/redis/go-redis") attrs := make([]attribute.KeyValue, 0, 8) @@ -145,10 +137,6 @@ func (th *tracingHook) ProcessPipelineHook( hook redis.ProcessPipelineHook, ) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { - if !trace.SpanFromContext(ctx).IsRecording() { - return hook(ctx, cmds) - } - fn, file, line := funcFileLine("github.com/redis/go-redis") attrs := make([]attribute.KeyValue, 0, 8) From bfea813687772854db05665c0712cda15e828461 Mon Sep 17 00:00:00 2001 From: Monkey Date: Wed, 29 May 2024 10:55:28 +0800 Subject: [PATCH 007/230] fix: fix #2681 (#2998) Signed-off-by: monkey92t --- options.go | 6 ++++++ osscluster.go | 18 ++++++++++-------- redis.go | 11 ++++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/options.go b/options.go index 23f07bc55c..6ed693a0b0 100644 --- a/options.go +++ b/options.go @@ -61,6 +61,12 @@ type Options struct { // before reconnecting. It should return the current username and password. CredentialsProvider func() (username string, password string) + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + // Database to be selected after connecting to the server. DB int diff --git a/osscluster.go b/osscluster.go index 97fc9392fc..e28cb1e391 100644 --- a/osscluster.go +++ b/osscluster.go @@ -62,10 +62,11 @@ type ClusterOptions struct { OnConnect func(ctx context.Context, cn *Conn) error - Protocol int - Username string - Password string - CredentialsProvider func() (username string, password string) + Protocol int + Username string + Password string + CredentialsProvider func() (username string, password string) + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) MaxRetries int MinRetryBackoff time.Duration @@ -272,10 +273,11 @@ func (opt *ClusterOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, - CredentialsProvider: opt.CredentialsProvider, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, MaxRetries: opt.MaxRetries, MinRetryBackoff: opt.MinRetryBackoff, diff --git a/redis.go b/redis.go index d25a0d3142..527afb677f 100644 --- a/redis.go +++ b/redis.go @@ -283,8 +283,13 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { } cn.Inited = true + var err error username, password := c.opt.Username, c.opt.Password - if c.opt.CredentialsProvider != nil { + if c.opt.CredentialsProviderContext != nil { + if username, password, err = c.opt.CredentialsProviderContext(ctx); err != nil { + return err + } + } else if c.opt.CredentialsProvider != nil { username, password = c.opt.CredentialsProvider() } @@ -300,7 +305,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err := conn.Hello(ctx, protocol, username, password, "").Err(); err == nil { + if err = conn.Hello(ctx, protocol, username, password, "").Err(); err == nil { auth = true } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal @@ -313,7 +318,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return err } - _, err := conn.Pipelined(ctx, func(pipe Pipeliner) error { + _, err = conn.Pipelined(ctx, func(pipe Pipeliner) error { if !auth && password != "" { if username != "" { pipe.AuthACL(ctx, username, password) From 4a1319fb04b08127ae3fc3ff7ca9b8aba10fb81a Mon Sep 17 00:00:00 2001 From: Vladimir Mihailenco Date: Fri, 7 Jun 2024 17:04:08 +0300 Subject: [PATCH 008/230] Release/v9.5.3 (#3018) * chore: ignore package.json * chore: release v9.5.3 (release.sh) --- example/del-keys-without-ttl/go.mod | 2 +- example/del-keys-without-ttl/go.sum | 4 ++-- example/hll/go.mod | 2 +- example/hll/go.sum | 4 ++-- example/lua-scripting/go.mod | 2 +- example/lua-scripting/go.sum | 4 ++-- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/redis-bloom/go.sum | 4 ++-- example/scan-struct/go.mod | 2 +- example/scan-struct/go.sum | 4 ++-- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- scripts/release.sh | 3 --- version.go | 2 +- 17 files changed, 25 insertions(+), 28 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 02955cd6a6..468d0a54fa 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 go.uber.org/zap v1.24.0 ) diff --git a/example/del-keys-without-ttl/go.sum b/example/del-keys-without-ttl/go.sum index 4f8d9abbff..e426a762bb 100644 --- a/example/del-keys-without-ttl/go.sum +++ b/example/del-keys-without-ttl/go.sum @@ -1,6 +1,6 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/example/hll/go.mod b/example/hll/go.mod index 27c66c2752..0126764ef4 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.1 +require github.com/redis/go-redis/v9 v9.5.3 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/hll/go.sum b/example/hll/go.sum index 813d21fd92..0e92df5e78 100644 --- a/example/hll/go.sum +++ b/example/hll/go.sum @@ -1,5 +1,5 @@ -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 03402b00e4..3f4c29d12b 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.1 +require github.com/redis/go-redis/v9 v9.5.3 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/lua-scripting/go.sum b/example/lua-scripting/go.sum index 813d21fd92..0e92df5e78 100644 --- a/example/lua-scripting/go.sum +++ b/example/lua-scripting/go.sum @@ -1,5 +1,5 @@ -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/example/otel/go.mod b/example/otel/go.mod index 86851af002..2beb75db9e 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.5.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.5.3 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index f152ff269a..09fb5bed4a 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.1 +require github.com/redis/go-redis/v9 v9.5.3 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/redis-bloom/go.sum b/example/redis-bloom/go.sum index 813d21fd92..0e92df5e78 100644 --- a/example/redis-bloom/go.sum +++ b/example/redis-bloom/go.sum @@ -1,5 +1,5 @@ -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 6bc8a69c45..c01e312933 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 ) require ( diff --git a/example/scan-struct/go.sum b/example/scan-struct/go.sum index 387a788614..6274a65f69 100644 --- a/example/scan-struct/go.sum +++ b/example/scan-struct/go.sum @@ -1,5 +1,5 @@ -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 02fb4d73dd..d623cef367 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.5.3 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 48caa794be..a035c86943 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index c5bafff8f5..587d3bc3a4 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.5.3 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 6e98bde964..fcc35b9bd9 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 ) require ( diff --git a/scripts/release.sh b/scripts/release.sh index cd4ddee539..fb7d58788c 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -60,9 +60,6 @@ do done sed --in-place "s/\(return \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./version.go -sed --in-place "s/\(\"version\": \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./package.json - -conventional-changelog -p angular -i CHANGELOG.md -s git checkout -b release/${TAG} master git add -u diff --git a/version.go b/version.go index e2c7f3e718..2ea7df99a7 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.5.1" + return "9.5.3" } From 0ef8e52e7299742cd807fef08c116ffe40b62142 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:08:35 +0300 Subject: [PATCH 009/230] Change redis version from 7.2 to 7.4 in makefile (#3034) * Change redis version from 7.2 to 7.4 * fix jsonGet test * Add 'watch' to client info * Remove jsonGet from Enterprise tests --- Makefile | 2 +- command.go | 3 +++ json_test.go | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ea5321f293..00cf1de584 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ build: testdata/redis: mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.2.1.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- https://download.redis.io/releases/redis-7.4-rc1.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis cd $< && make all diff --git a/command.go b/command.go index c6cd9db6d6..6bce5a3444 100644 --- a/command.go +++ b/command.go @@ -4997,6 +4997,7 @@ type ClientInfo struct { PSub int // number of pattern matching subscriptions SSub int // redis version 7.0.3, number of shard channel subscriptions Multi int // number of commands in a MULTI/EXEC context + Watch int // redis version 7.4 RC1, number of keys this client is currently watching. QueryBuf int // qbuf, query buffer length (0 means no query pending) QueryBufFree int // qbuf-free, free space of the query buffer (0 means the buffer is full) ArgvMem int // incomplete arguments for the next command (already extracted from query buffer) @@ -5149,6 +5150,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.SSub, err = strconv.Atoi(val) case "multi": info.Multi, err = strconv.Atoi(val) + case "watch": + info.Watch, err = strconv.Atoi(val) case "qbuf": info.QueryBuf, err = strconv.Atoi(val) case "qbuf-free": diff --git a/json_test.go b/json_test.go index 89b4b09955..2f559b992b 100644 --- a/json_test.go +++ b/json_test.go @@ -247,18 +247,18 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(cmd.Val()).To(Equal("OK")) }) - It("should JSONGet", Label("json.get", "json"), func() { + It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() { res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal("OK")) res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[-{--"a":1,--"b":2-}]`)) + Expect(res).To(Equal(`{-"a":1,-"b":2}`)) res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[~-{~--"a":!1,~--"b":!2~-}~]`)) + Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`)) }) It("should JSONMerge", Label("json.merge", "json"), func() { From 8409fa54e209112b1072d5e8f204bc4c93f41a7d Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:59:27 +0300 Subject: [PATCH 010/230] Support Hash-field expiration commands (#2991) * Add HExpire command * Add HPExpire, HexpireAt, HPExpireAt, HTTL, HPTTL, HPersist,HExpireTime, HPExpireTIme, HGetF, HSetF commands * add docstring * add tests and fix commands * modify commands * api changes * fix tests * remove tests from RE --- commands_test.go | 148 ++++++++++++++++++++++++++++ hash_commands.go | 250 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 397 insertions(+), 1 deletion(-) diff --git a/commands_test.go b/commands_test.go index d30a9d8bb3..b0224449b2 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2430,6 +2430,154 @@ var _ = Describe("Commands", func() { Equal([]redis.KeyValue{{Key: "key2", Value: "hello2"}}), )) }) + + It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + res, err := client.HExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err = client.HExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + _, err := client.HPExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPersist", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{-1, -1, -2})) + + res, err = client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -1, -2})) + }) + + It("should HExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HExpireTime(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(BeNumerically("~", time.Now().Add(10*time.Second).Unix(), 1)) + }) + + It("should HPExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPExpireTime(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo([]int64{time.Now().Add(10 * time.Second).UnixMilli(), -1, -2})) + }) + + It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HTTL(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{10, -1, -2})) + }) + + It("should HPTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPTTL(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1)) + }) }) Describe("hyperloglog", func() { diff --git a/hash_commands.go b/hash_commands.go index 2c62a75ad9..59fbb8656b 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -1,6 +1,9 @@ package redis -import "context" +import ( + "context" + "time" +) type HashCmdable interface { HDel(ctx context.Context, key string, fields ...string) *IntCmd @@ -172,3 +175,248 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str _ = c(ctx, cmd) return cmd } + +type HExpireArgs struct { + NX bool + XX bool + GT bool + LT bool +} + +// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. +// For more information - https://redis.io/commands/hexpire/ +func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRE", key, expiration, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. +// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. +// For more information - https://redis.io/commands/hexpire/ +func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRE", key, expiration} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds. +// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields. +// The command modifies the standard time.Duration to milliseconds for the Redis command. +// For more information - https://redis.io/commands/hpexpire/ +func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds. +// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields. +// The command sets absolute expiration times based on the UNIX timestamp provided. +// For more information - https://redis.io/commands/hexpireat/ +func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { + + args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIREAT", key, tm.Unix()} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds. +// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds. +// For more information - https://redis.io/commands/hpexpireat/ +func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond)} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPersist - Removes the expiration time from specified fields in a hash. +// Accepts a key and the fields themselves. +// This command ensures that each field specified will have its expiration removed if present. +// For more information - https://redis.io/commands/hpersist/ +func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds. +// Requires a key and the fields themselves to fetch their expiration timestamps. +// This command returns the expiration times for each field or error/status codes for each field as specified. +// For more information - https://redis.io/commands/hexpiretime/ +func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds. +// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters. +// Provides the expiration timestamp for each field in milliseconds. +// For more information - https://redis.io/commands/hexpiretime/ +func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds. +// Requires a key and the fields themselves. It returns the TTL for each specified field. +// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate. +// For more information - https://redis.io/commands/httl/ +func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HTTL", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds. +// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields. +// This command provides the TTL in milliseconds for each field or returns error/status codes as needed. +// For more information - https://redis.io/commands/hpttl/ +func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPTTL", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} From f95779d8eef7da49748dcdf83241e3ae58270bbb Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Thu, 20 Jun 2024 01:00:09 +0300 Subject: [PATCH 011/230] Added test case for CLIENT KILL with MAXAGE option (#2971) * Added test case for CLIENT KILL with MAXAGE option * Fixed sleep value * Added additional condition to kill specific connection * Test commit * Test commit * Updated test case to handle timeouts --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- commands_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/commands_test.go b/commands_test.go index b0224449b2..5d7880434f 100644 --- a/commands_test.go +++ b/commands_test.go @@ -193,6 +193,40 @@ var _ = Describe("Commands", func() { Expect(r.Val()).To(Equal(int64(0))) }) + It("should ClientKillByFilter with MAXAGE", func() { + var s []string + started := make(chan bool) + done := make(chan bool) + + go func() { + defer GinkgoRecover() + + started <- true + blpop := client.BLPop(ctx, 0, "list") + Expect(blpop.Val()).To(Equal(s)) + done <- true + }() + <-started + + select { + case <-done: + Fail("BLPOP is not blocked.") + case <-time.After(2 * time.Second): + // ok + } + + killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") + Expect(killed.Err()).NotTo(HaveOccurred()) + Expect(killed.Val()).To(Equal(int64(2))) + + select { + case <-done: + // ok + case <-time.After(time.Second): + Fail("BLPOP is still blocked.") + } + }) + It("should ClientID", func() { err := client.ClientID(ctx).Err() Expect(err).NotTo(HaveOccurred()) From f7411e0f14302b772881f2b0d526d58ca17ba90b Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Thu, 20 Jun 2024 01:24:45 +0300 Subject: [PATCH 012/230] Support NOVALUES parameter for HSCAN (#2925) * Support NOVALUES parameter for HSCAN Issue #2919 The NOVALUES parameter instructs HSCAN to only return the hash keys, without values. * Update hash_commands.go --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- commands_test.go | 22 ++++++++++++++++++++-- hash_commands.go | 15 +++++++++++++++ iterator_test.go | 16 ++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/commands_test.go b/commands_test.go index 5d7880434f..dea3d5f535 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1134,8 +1134,26 @@ var _ = Describe("Commands", func() { keys, cursor, err := client.HScan(ctx, "myhash", 0, "", 0).Result() Expect(err).NotTo(HaveOccurred()) - Expect(keys).NotTo(BeEmpty()) - Expect(cursor).NotTo(BeZero()) + // If we don't get at least two items back, it's really strange. + Expect(cursor).To(BeNumerically(">=", 2)) + Expect(len(keys)).To(BeNumerically(">=", 2)) + Expect(keys[0]).To(HavePrefix("key")) + Expect(keys[1]).To(Equal("hello")) + }) + + It("should HScan without values", func() { + for i := 0; i < 1000; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + keys, cursor, err := client.HScanNoValues(ctx, "myhash", 0, "", 0).Result() + Expect(err).NotTo(HaveOccurred()) + // If we don't get at least two items back, it's really strange. + Expect(cursor).To(BeNumerically(">=", 2)) + Expect(len(keys)).To(BeNumerically(">=", 2)) + Expect(keys[0]).To(HavePrefix("key")) + Expect(keys[1]).To(HavePrefix("key")) }) It("should ZScan", func() { diff --git a/hash_commands.go b/hash_commands.go index 59fbb8656b..ef69064e0d 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -19,6 +19,7 @@ type HashCmdable interface { HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd + HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd HVals(ctx context.Context, key string) *StringSliceCmd HRandField(ctx context.Context, key string, count int) *StringSliceCmd HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd @@ -176,6 +177,20 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str return cmd } +func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { + args := []interface{}{"hscan", key, cursor} + if match != "" { + args = append(args, "match", match) + } + if count > 0 { + args = append(args, "count", count) + } + args = append(args, "novalues") + cmd := NewScanCmd(ctx, c, args...) + _ = c(ctx, cmd) + return cmd +} + type HExpireArgs struct { NX bool XX bool diff --git a/iterator_test.go b/iterator_test.go index ccd9414780..dfa8fbbed5 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -96,6 +96,22 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(HaveLen(71 * 2)) Expect(vals).To(ContainElement("K01")) Expect(vals).To(ContainElement("K71")) + Expect(vals).To(ContainElement("x")) + }) + + It("should hscan without values across multiple pages", func() { + Expect(hashSeed(71)).NotTo(HaveOccurred()) + + var vals []string + iter := client.HScanNoValues(ctx, hashKey, 0, "", 10).Iterator() + for iter.Next(ctx) { + vals = append(vals, iter.Val()) + } + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(71)) + Expect(vals).To(ContainElement("K01")) + Expect(vals).To(ContainElement("K71")) + Expect(vals).NotTo(ContainElement("x")) }) It("should scan to page borders", func() { From ef84e83664ddf1da94979c37d2bac817309eaf32 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 20 Jun 2024 02:25:49 +0300 Subject: [PATCH 013/230] remove tests from RE (#3035) --- commands_test.go | 8 +++++--- iterator_test.go | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/commands_test.go b/commands_test.go index dea3d5f535..641757d239 100644 --- a/commands_test.go +++ b/commands_test.go @@ -193,7 +193,7 @@ var _ = Describe("Commands", func() { Expect(r.Val()).To(Equal(int64(0))) }) - It("should ClientKillByFilter with MAXAGE", func() { + It("should ClientKillByFilter with MAXAGE", Label("NonRedisEnterprise"), func() { var s []string started := make(chan bool) done := make(chan bool) @@ -217,7 +217,7 @@ var _ = Describe("Commands", func() { killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(Equal(int64(2))) + Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)))) select { case <-done: @@ -1141,7 +1141,7 @@ var _ = Describe("Commands", func() { Expect(keys[1]).To(Equal("hello")) }) - It("should HScan without values", func() { + It("should HScan without values", Label("NonRedisEnterprise"), func() { for i := 0; i < 1000; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -1154,6 +1154,8 @@ var _ = Describe("Commands", func() { Expect(len(keys)).To(BeNumerically(">=", 2)) Expect(keys[0]).To(HavePrefix("key")) Expect(keys[1]).To(HavePrefix("key")) + Expect(keys).NotTo(BeEmpty()) + Expect(cursor).NotTo(BeZero()) }) It("should ZScan", func() { diff --git a/iterator_test.go b/iterator_test.go index dfa8fbbed5..472ce38a7d 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -99,7 +99,7 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(ContainElement("x")) }) - It("should hscan without values across multiple pages", func() { + It("should hscan without values across multiple pages", Label("NonRedisEnterprise"), func() { Expect(hashSeed(71)).NotTo(HaveOccurred()) var vals []string From 0e5453d7df8c90cc9cd534cd55608ae605650b0d Mon Sep 17 00:00:00 2001 From: b1ron <80292536+b1ron@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:25:51 +0200 Subject: [PATCH 014/230] Add support for XREAD last entry (#3005) * add support for XREAD last entry * handle reading from multiple streams * add test to ensure we block for empty stream * small tweak * add an option to XReadArgs instead * modify test comment * small preallocation optimization * Changed argument to generic ID, skip tests on Enterprise * Fix test case * Updated expiration command --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: vladvildanov --- commands_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++-- stream_commands.go | 8 ++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/commands_test.go b/commands_test.go index 641757d239..edc9569435 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2588,13 +2588,14 @@ var _ = Describe("Commands", func() { Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + expireAt := time.Now().Add(10 * time.Second) + res, err := client.HPExpireAt(ctx, "myhash", expireAt, "key1", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, -2})) res, err = client.HPExpireTime(ctx, "myhash", "key1", "key2", "key200").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo([]int64{time.Now().Add(10 * time.Second).UnixMilli(), -1, -2})) + Expect(res).To(BeEquivalentTo([]int64{expireAt.UnixMilli(), -1, -2})) }) It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { @@ -5888,6 +5889,78 @@ var _ = Describe("Commands", func() { Expect(err).To(Equal(redis.Nil)) }) + It("should XRead LastEntry", Label("NonRedisEnterprise"), func() { + res, err := client.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"stream"}, + Count: 2, // we expect 1 message + ID: "+", + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]redis.XStream{ + { + Stream: "stream", + Messages: []redis.XMessage{ + {ID: "3-0", Values: map[string]interface{}{"tres": "troix"}}, + }, + }, + })) + }) + + It("should XRead LastEntry from two streams", Label("NonRedisEnterprise"), func() { + res, err := client.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"stream", "stream"}, + ID: "+", + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]redis.XStream{ + { + Stream: "stream", + Messages: []redis.XMessage{ + {ID: "3-0", Values: map[string]interface{}{"tres": "troix"}}, + }, + }, + { + Stream: "stream", + Messages: []redis.XMessage{ + {ID: "3-0", Values: map[string]interface{}{"tres": "troix"}}, + }, + }, + })) + }) + + It("should XRead LastEntry blocks", Label("NonRedisEnterprise"), func() { + start := time.Now() + go func() { + defer GinkgoRecover() + + time.Sleep(100 * time.Millisecond) + id, err := client.XAdd(ctx, &redis.XAddArgs{ + Stream: "empty", + ID: "4-0", + Values: map[string]interface{}{"quatro": "quatre"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(id).To(Equal("4-0")) + }() + + res, err := client.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"empty"}, + Block: 500 * time.Millisecond, + ID: "+", + }).Result() + Expect(err).NotTo(HaveOccurred()) + // Ensure that the XRead call with LastEntry option blocked for at least 100ms. + Expect(time.Since(start)).To(BeNumerically(">=", 100*time.Millisecond)) + Expect(res).To(Equal([]redis.XStream{ + { + Stream: "empty", + Messages: []redis.XMessage{ + {ID: "4-0", Values: map[string]interface{}{"quatro": "quatre"}}, + }, + }, + })) + }) + Describe("group", func() { BeforeEach(func() { err := client.XGroupCreate(ctx, "stream", "group", "0").Err() diff --git a/stream_commands.go b/stream_commands.go index 1ad33740ce..6d7b229224 100644 --- a/stream_commands.go +++ b/stream_commands.go @@ -137,10 +137,11 @@ type XReadArgs struct { Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2 Count int64 Block time.Duration + ID string } func (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd { - args := make([]interface{}, 0, 6+len(a.Streams)) + args := make([]interface{}, 0, 2*len(a.Streams)+6) args = append(args, "xread") keyPos := int8(1) @@ -159,6 +160,11 @@ func (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd { for _, s := range a.Streams { args = append(args, s) } + if a.ID != "" { + for range a.Streams { + args = append(args, a.ID) + } + } cmd := NewXStreamSliceCmd(ctx, args...) if a.Block >= 0 { From 9edb041e7fd1c3c5b1e0df77856b25807ca8f372 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:58:08 +0800 Subject: [PATCH 015/230] chore(deps): bump golang.org/x/net in /example/otel (#3000) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Monkey --- example/otel/go.mod | 4 ++-- example/otel/go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/otel/go.mod b/example/otel/go.mod index 2beb75db9e..fea4e72a51 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -34,8 +34,8 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index 4a481d6efd..5fb4c4588f 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -46,10 +46,10 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 3d4bc2091ef18ae35d785ea8e9666e888293e0c5 Mon Sep 17 00:00:00 2001 From: Andrew Haines Date: Fri, 21 Jun 2024 04:00:16 +0100 Subject: [PATCH 016/230] Add `(*StatusCmd).Bytes()` method (#3030) Signed-off-by: Andrew Haines Co-authored-by: Monkey --- command.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/command.go b/command.go index 6bce5a3444..59ba089695 100644 --- a/command.go +++ b/command.go @@ -573,6 +573,10 @@ func (cmd *StatusCmd) Result() (string, error) { return cmd.val, cmd.err } +func (cmd *StatusCmd) Bytes() ([]byte, error) { + return util.StringToBytes(cmd.val), cmd.err +} + func (cmd *StatusCmd) String() string { return cmdString(cmd, cmd.val) } From 31068bbf2b1be151849eeb6a0a80a39242486a14 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:13:06 +0300 Subject: [PATCH 017/230] RediSearch Support (#2801) * Add RediSearch Support * searach * Add RediSearch commands and tests * Adding more tests and fixing commands * Remove unnecessary additions * fixing tests * fixing tests * fixing tests * fixing FTConfig dialect test * fix commects * make enum for field types * Support resp 2 * fix golang ci * fix ftinfo --------- Co-authored-by: Chayim --- .github/wordlist.txt | 3 +- command.go | 59 ++ commands.go | 1 + internal/util.go | 45 + search_commands.go | 2192 ++++++++++++++++++++++++++++++++++++++++++ search_test.go | 1136 ++++++++++++++++++++++ 6 files changed, 3435 insertions(+), 1 deletion(-) create mode 100644 search_commands.go create mode 100644 search_test.go diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 52fdc1bcfb..dceddff46a 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -57,4 +57,5 @@ url variadic RedisStack RedisGears -RedisTimeseries \ No newline at end of file +RedisTimeseries +RediSearch diff --git a/command.go b/command.go index 59ba089695..9ae97a95ae 100644 --- a/command.go +++ b/command.go @@ -3787,6 +3787,65 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error { return nil } +// ----------------------------------------------------------------------- +// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}. +type MapMapStringInterfaceCmd struct { + baseCmd + val map[string]interface{} +} + +func NewMapMapStringInterfaceCmd(ctx context.Context, args ...interface{}) *MapMapStringInterfaceCmd { + return &MapMapStringInterfaceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *MapMapStringInterfaceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *MapMapStringInterfaceCmd) SetVal(val map[string]interface{}) { + cmd.val = val +} + +func (cmd *MapMapStringInterfaceCmd) Result() (map[string]interface{}, error) { + return cmd.val, cmd.err +} + +func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { + return cmd.val +} + +func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + data := make(map[string]interface{}, n/2) + for i := 0; i < n; i += 2 { + _, err := rd.ReadArrayLen() + if err != nil { + cmd.err = err + } + key, err := rd.ReadString() + if err != nil { + cmd.err = err + } + value, err := rd.ReadString() + if err != nil { + cmd.err = err + } + data[key] = value + } + + cmd.val = data + return nil +} + //----------------------------------------------------------------------- type MapStringInterfaceSliceCmd struct { diff --git a/commands.go b/commands.go index db59594469..034daa2350 100644 --- a/commands.go +++ b/commands.go @@ -220,6 +220,7 @@ type Cmdable interface { ProbabilisticCmdable PubSubCmdable ScriptingFunctionsCmdable + SearchCmdable SetCmdable SortedSetCmdable StringCmdable diff --git a/internal/util.go b/internal/util.go index 235a91afa9..cc1bff24e6 100644 --- a/internal/util.go +++ b/internal/util.go @@ -3,6 +3,7 @@ package internal import ( "context" "net" + "strconv" "strings" "time" @@ -81,3 +82,47 @@ func GetAddr(addr string) string { } return net.JoinHostPort(addr[:ind], addr[ind+1:]) } + +func ToInteger(val interface{}) int { + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case string: + i, _ := strconv.Atoi(v) + return i + default: + return 0 + } +} + +func ToFloat(val interface{}) float64 { + switch v := val.(type) { + case float64: + return v + case string: + f, _ := strconv.ParseFloat(v, 64) + return f + default: + return 0.0 + } +} + +func ToString(val interface{}) string { + if str, ok := val.(string); ok { + return str + } + return "" +} + +func ToStringSlice(val interface{}) []string { + if arr, ok := val.([]interface{}); ok { + result := make([]string, len(arr)) + for i, v := range arr { + result[i] = ToString(v) + } + return result + } + return nil +} diff --git a/search_commands.go b/search_commands.go new file mode 100644 index 0000000000..8214a570be --- /dev/null +++ b/search_commands.go @@ -0,0 +1,2192 @@ +package redis + +import ( + "context" + "fmt" + "strconv" + + "github.com/redis/go-redis/v9/internal" + "github.com/redis/go-redis/v9/internal/proto" +) + +type SearchCmdable interface { + FT_List(ctx context.Context) *StringSliceCmd + FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd + FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd + FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd + FTAliasDel(ctx context.Context, alias string) *StatusCmd + FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd + FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd + FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd + FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd + FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd + FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd + FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd + FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd + FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd + FTDictDump(ctx context.Context, dict string) *StringSliceCmd + FTDropIndex(ctx context.Context, index string) *StatusCmd + FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd + FTExplain(ctx context.Context, index string, query string) *StringCmd + FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd + FTInfo(ctx context.Context, index string) *FTInfoCmd + FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd + FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd + FTSearch(ctx context.Context, index string, query string) *FTSearchCmd + FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd + FTSynDump(ctx context.Context, index string) *FTSynDumpCmd + FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd + FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd + FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd +} + +type FTCreateOptions struct { + OnHash bool + OnJSON bool + Prefix []interface{} + Filter string + DefaultLanguage string + LanguageField string + Score float64 + ScoreField string + PayloadField string + MaxTextFields int + NoOffsets bool + Temporary int + NoHL bool + NoFields bool + NoFreqs bool + StopWords []interface{} + SkipInitalScan bool +} + +type FieldSchema struct { + FieldName string + As string + FieldType SearchFieldType + Sortable bool + UNF bool + NoStem bool + NoIndex bool + PhoneticMatcher string + Weight float64 + Seperator string + CaseSensitive bool + WithSuffixtrie bool + VectorArgs *FTVectorArgs + GeoShapeFieldType string +} + +type FTVectorArgs struct { + FlatOptions *FTFlatOptions + HNSWOptions *FTHNSWOptions +} + +type FTFlatOptions struct { + Type string + Dim int + DistanceMetric string + InitialCapacity int + BlockSize int +} + +type FTHNSWOptions struct { + Type string + Dim int + DistanceMetric string + InitialCapacity int + MaxEdgesPerNode int + MaxAllowedEdgesPerNode int + EFRunTime int + Epsilon float64 +} + +type FTDropIndexOptions struct { + DeleteDocs bool +} + +type SpellCheckTerms struct { + Include bool + Exclude bool + Dictionary string +} + +type FTExplainOptions struct { + Dialect string +} + +type FTSynUpdateOptions struct { + SkipInitialScan bool +} + +type SearchAggregator int + +const ( + SearchInvalid = SearchAggregator(iota) + SearchAvg + SearchSum + SearchMin + SearchMax + SearchCount + SearchCountDistinct + SearchCountDistinctish + SearchStdDev + SearchQuantile + SearchToList + SearchFirstValue + SearchRandomSample +) + +func (a SearchAggregator) String() string { + switch a { + case SearchInvalid: + return "" + case SearchAvg: + return "AVG" + case SearchSum: + return "SUM" + case SearchMin: + return "MIN" + case SearchMax: + return "MAX" + case SearchCount: + return "COUNT" + case SearchCountDistinct: + return "COUNT_DISTINCT" + case SearchCountDistinctish: + return "COUNT_DISTINCTISH" + case SearchStdDev: + return "STDDEV" + case SearchQuantile: + return "QUANTILE" + case SearchToList: + return "TOLIST" + case SearchFirstValue: + return "FIRST_VALUE" + case SearchRandomSample: + return "RANDOM_SAMPLE" + default: + return "" + } +} + +type SearchFieldType int + +const ( + SearchFieldTypeInvalid = SearchFieldType(iota) + SearchFieldTypeNumeric + SearchFieldTypeTag + SearchFieldTypeText + SearchFieldTypeGeo + SearchFieldTypeVector + SearchFieldTypeGeoShape +) + +func (t SearchFieldType) String() string { + switch t { + case SearchFieldTypeInvalid: + return "" + case SearchFieldTypeNumeric: + return "NUMERIC" + case SearchFieldTypeTag: + return "TAG" + case SearchFieldTypeText: + return "TEXT" + case SearchFieldTypeGeo: + return "GEO" + case SearchFieldTypeVector: + return "VECTOR" + case SearchFieldTypeGeoShape: + return "GEOSHAPE" + default: + return "TEXT" + } +} + +// Each AggregateReducer have different args. +// Please follow https://redis.io/docs/interact/search-and-query/search/aggregations/#supported-groupby-reducers for more information. +type FTAggregateReducer struct { + Reducer SearchAggregator + Args []interface{} + As string +} + +type FTAggregateGroupBy struct { + Fields []interface{} + Reduce []FTAggregateReducer +} + +type FTAggregateSortBy struct { + FieldName string + Asc bool + Desc bool +} + +type FTAggregateApply struct { + Field string + As string +} + +type FTAggregateLoad struct { + Field string + As string +} + +type FTAggregateWithCursor struct { + Count int + MaxIdle int +} + +type FTAggregateOptions struct { + Verbatim bool + LoadAll bool + Load []FTAggregateLoad + Timeout int + GroupBy []FTAggregateGroupBy + SortBy []FTAggregateSortBy + SortByMax int + Apply []FTAggregateApply + LimitOffset int + Limit int + Filter string + WithCursor bool + WithCursorOptions *FTAggregateWithCursor + Params map[string]interface{} + DialectVersion int +} + +type FTSearchFilter struct { + FieldName interface{} + Min interface{} + Max interface{} +} + +type FTSearchGeoFilter struct { + FieldName string + Longitude float64 + Latitude float64 + Radius float64 + Unit string +} + +type FTSearchReturn struct { + FieldName string + As string +} + +type FTSearchSortBy struct { + FieldName string + Asc bool + Desc bool +} + +type FTSearchOptions struct { + NoContent bool + Verbatim bool + NoStopWrods bool + WithScores bool + WithPayloads bool + WithSortKeys bool + Filters []FTSearchFilter + GeoFilter []FTSearchGeoFilter + InKeys []interface{} + InFields []interface{} + Return []FTSearchReturn + Slop int + Timeout int + InOrder bool + Language string + Expander string + Scorer string + ExplainScore bool + Payload string + SortBy []FTSearchSortBy + SortByWithCount bool + LimitOffset int + Limit int + Params map[string]interface{} + DialectVersion int +} + +type FTSynDumpResult struct { + Term string + Synonyms []string +} + +type FTSynDumpCmd struct { + baseCmd + val []FTSynDumpResult +} + +type FTAggregateResult struct { + Total int + Rows []AggregateRow +} + +type AggregateRow struct { + Fields map[string]interface{} +} + +type AggregateCmd struct { + baseCmd + val *FTAggregateResult +} + +type FTInfoResult struct { + IndexErrors IndexErrors + Attributes []FTAttribute + BytesPerRecordAvg string + Cleaning int + CursorStats CursorStats + DialectStats map[string]int + DocTableSizeMB float64 + FieldStatistics []FieldStatistic + GCStats GCStats + GeoshapesSzMB float64 + HashIndexingFailures int + IndexDefinition IndexDefinition + IndexName string + IndexOptions []string + Indexing int + InvertedSzMB float64 + KeyTableSizeMB float64 + MaxDocID int + NumDocs int + NumRecords int + NumTerms int + NumberOfUses int + OffsetBitsPerRecordAvg string + OffsetVectorsSzMB float64 + OffsetsPerTermAvg string + PercentIndexed float64 + RecordsPerDocAvg string + SortableValuesSizeMB float64 + TagOverheadSzMB float64 + TextOverheadSzMB float64 + TotalIndexMemorySzMB float64 + TotalIndexingTime int + TotalInvertedIndexBlocks int + VectorIndexSzMB float64 +} + +type IndexErrors struct { + IndexingFailures int + LastIndexingError string + LastIndexingErrorKey string +} + +type FTAttribute struct { + Identifier string + Attribute string + Type string + Weight float64 + Sortable bool + NoStem bool + NoIndex bool + UNF bool + PhoneticMatcher string + CaseSensitive bool + WithSuffixtrie bool +} + +type CursorStats struct { + GlobalIdle int + GlobalTotal int + IndexCapacity int + IndexTotal int +} + +type FieldStatistic struct { + Identifier string + Attribute string + IndexErrors IndexErrors +} + +type GCStats struct { + BytesCollected int + TotalMsRun int + TotalCycles int + AverageCycleTimeMs string + LastRunTimeMs int + GCNumericTreesMissed int + GCBlocksDenied int +} + +type IndexDefinition struct { + KeyType string + Prefixes []string + DefaultScore float64 +} + +type FTSpellCheckOptions struct { + Distance int + Terms *FTSpellCheckTerms + Dialect int +} + +type FTSpellCheckTerms struct { + Inclusion string // Either "INCLUDE" or "EXCLUDE" + Dictionary string + Terms []interface{} +} + +type SpellCheckResult struct { + Term string + Suggestions []SpellCheckSuggestion +} + +type SpellCheckSuggestion struct { + Score float64 + Suggestion string +} + +type FTSearchResult struct { + Total int + Docs []Document +} + +type Document struct { + ID string + Score *float64 + Payload *string + SortKey *string + Fields map[string]string +} + +type AggregateQuery []interface{} + +// FT_List - Lists all the existing indexes in the database. +// For more information, please refer to the Redis documentation: +// [FT._LIST]: (https://redis.io/commands/ft._list/) +func (c cmdable) FT_List(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT._LIST") + _ = c(ctx, cmd) + return cmd +} + +// FTAggregate - Performs a search query on an index and applies a series of aggregate transformations to the result. +// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. +// For more information, please refer to the Redis documentation: +// [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/) +func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *MapStringInterfaceCmd { + args := []interface{}{"FT.AGGREGATE", index, query} + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { + queryArgs := []interface{}{query} + if options != nil { + if options.Verbatim { + queryArgs = append(queryArgs, "VERBATIM") + } + if options.LoadAll && options.Load != nil { + panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + if options.LoadAll { + queryArgs = append(queryArgs, "LOAD", "*") + } + if options.Load != nil { + queryArgs = append(queryArgs, "LOAD", len(options.Load)) + for _, load := range options.Load { + queryArgs = append(queryArgs, load.Field) + if load.As != "" { + queryArgs = append(queryArgs, "AS", load.As) + } + } + } + if options.Timeout > 0 { + queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) + queryArgs = append(queryArgs, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + queryArgs = append(queryArgs, "REDUCE") + queryArgs = append(queryArgs, reducer.Reducer.String()) + if reducer.Args != nil { + queryArgs = append(queryArgs, len(reducer.Args)) + queryArgs = append(queryArgs, reducer.Args...) + } else { + queryArgs = append(queryArgs, 0) + } + if reducer.As != "" { + queryArgs = append(queryArgs, "AS", reducer.As) + } + } + } + } + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + queryArgs = append(queryArgs, len(sortByOptions)) + queryArgs = append(queryArgs, sortByOptions...) + } + if options.SortByMax > 0 { + queryArgs = append(queryArgs, "MAX", options.SortByMax) + } + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } + } + if options.LimitOffset > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) + } + if options.Limit > 0 { + queryArgs = append(queryArgs, options.Limit) + } + if options.Filter != "" { + queryArgs = append(queryArgs, "FILTER", options.Filter) + } + if options.WithCursor { + queryArgs = append(queryArgs, "WITHCURSOR") + if options.WithCursorOptions != nil { + if options.WithCursorOptions.Count > 0 { + queryArgs = append(queryArgs, "COUNT", options.WithCursorOptions.Count) + } + if options.WithCursorOptions.MaxIdle > 0 { + queryArgs = append(queryArgs, "MAXIDLE", options.WithCursorOptions.MaxIdle) + } + } + } + if options.Params != nil { + queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + queryArgs = append(queryArgs, key, value) + } + } + if options.DialectVersion > 0 { + queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } + } + return queryArgs +} + +func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { + if len(data) == 0 { + return nil, fmt.Errorf("no data returned") + } + + total, ok := data[0].(int64) + if !ok { + return nil, fmt.Errorf("invalid total format") + } + + rows := make([]AggregateRow, 0, len(data)-1) + for _, row := range data[1:] { + fields, ok := row.([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid row format") + } + + rowMap := make(map[string]interface{}) + for i := 0; i < len(fields); i += 2 { + key, ok := fields[i].(string) + if !ok { + return nil, fmt.Errorf("invalid field key format") + } + value := fields[i+1] + rowMap[key] = value + } + rows = append(rows, AggregateRow{Fields: rowMap}) + } + + result := &FTAggregateResult{ + Total: int(total), + Rows: rows, + } + return result, nil +} + +func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd { + return &AggregateCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *AggregateCmd) SetVal(val *FTAggregateResult) { + cmd.val = val +} + +func (cmd *AggregateCmd) Val() *FTAggregateResult { + return cmd.val +} + +func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) { + return cmd.val, cmd.err +} + +func (cmd *AggregateCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = ProcessAggregateResult(data) + if err != nil { + cmd.err = err + } + return nil +} + +// FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result. +// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. +// This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion. +// For more information, please refer to the Redis documentation: +// [FT.AGGREGATE]: (https://redis.io/commands/ft.aggregate/) +func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd { + args := []interface{}{"FT.AGGREGATE", index, query} + if options != nil { + if options.Verbatim { + args = append(args, "VERBATIM") + } + if options.LoadAll && options.Load != nil { + panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + if options.LoadAll { + args = append(args, "LOAD", "*") + } + if options.Load != nil { + args = append(args, "LOAD", len(options.Load)) + for _, load := range options.Load { + args = append(args, load.Field) + if load.As != "" { + args = append(args, "AS", load.As) + } + } + } + if options.Timeout > 0 { + args = append(args, "TIMEOUT", options.Timeout) + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + args = append(args, "GROUPBY", len(groupBy.Fields)) + args = append(args, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + args = append(args, "REDUCE") + args = append(args, reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } + } + } + if options.SortBy != nil { + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + } + if options.SortByMax > 0 { + args = append(args, "MAX", options.SortByMax) + } + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) + } + } + if options.LimitOffset > 0 { + args = append(args, "LIMIT", options.LimitOffset) + } + if options.Limit > 0 { + args = append(args, options.Limit) + } + if options.Filter != "" { + args = append(args, "FILTER", options.Filter) + } + if options.WithCursor { + args = append(args, "WITHCURSOR") + if options.WithCursorOptions != nil { + if options.WithCursorOptions.Count > 0 { + args = append(args, "COUNT", options.WithCursorOptions.Count) + } + if options.WithCursorOptions.MaxIdle > 0 { + args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle) + } + } + } + if options.Params != nil { + args = append(args, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + args = append(args, key, value) + } + } + if options.DialectVersion > 0 { + args = append(args, "DIALECT", options.DialectVersion) + } + } + + cmd := NewAggregateCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasAdd - Adds an alias to an index. +// The 'index' parameter specifies the index to which the alias is added, and the 'alias' parameter specifies the alias. +// For more information, please refer to the Redis documentation: +// [FT.ALIASADD]: (https://redis.io/commands/ft.aliasadd/) +func (c cmdable) FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd { + args := []interface{}{"FT.ALIASADD", alias, index} + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasDel - Removes an alias from an index. +// The 'alias' parameter specifies the alias to be removed. +// For more information, please refer to the Redis documentation: +// [FT.ALIASDEL]: (https://redis.io/commands/ft.aliasdel/) +func (c cmdable) FTAliasDel(ctx context.Context, alias string) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.ALIASDEL", alias) + _ = c(ctx, cmd) + return cmd +} + +// FTAliasUpdate - Updates an alias to an index. +// The 'index' parameter specifies the index to which the alias is updated, and the 'alias' parameter specifies the alias. +// If the alias already exists for a different index, it updates the alias to point to the specified index instead. +// For more information, please refer to the Redis documentation: +// [FT.ALIASUPDATE]: (https://redis.io/commands/ft.aliasupdate/) +func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.ALIASUPDATE", alias, index) + _ = c(ctx, cmd) + return cmd +} + +// FTAlter - Alters the definition of an existing index. +// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan. +// The 'definition' parameter specifies the new definition for the index. +// For more information, please refer to the Redis documentation: +// [FT.ALTER]: (https://redis.io/commands/ft.alter/) +func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd { + args := []interface{}{"FT.ALTER", index} + if skipInitalScan { + args = append(args, "SKIPINITIALSCAN") + } + args = append(args, "SCHEMA", "ADD") + args = append(args, definition...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTConfigGet - Retrieves the value of a RediSearch configuration parameter. +// The 'option' parameter specifies the configuration parameter to retrieve. +// For more information, please refer to the Redis documentation: +// [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/) +func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd { + cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) + _ = c(ctx, cmd) + return cmd +} + +// FTConfigSet - Sets the value of a RediSearch configuration parameter. +// The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value. +// For more information, please refer to the Redis documentation: +// [FT.CONFIG SET]: (https://redis.io/commands/ft.config-set/) +func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) + _ = c(ctx, cmd) + return cmd +} + +// FTCreate - Creates a new index with the given options and schema. +// The 'index' parameter specifies the name of the index to create. +// The 'options' parameter specifies various options for the index, such as: +// whether to index hashes or JSONs, prefixes, filters, default language, score, score field, payload field, etc. +// The 'schema' parameter specifies the schema for the index, which includes the field name, field type, etc. +// For more information, please refer to the Redis documentation: +// [FT.CREATE]: (https://redis.io/commands/ft.create/) +func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd { + args := []interface{}{"FT.CREATE", index} + if options != nil { + if options.OnHash && !options.OnJSON { + args = append(args, "ON", "HASH") + } + if options.OnJSON && !options.OnHash { + args = append(args, "ON", "JSON") + } + if options.OnHash && options.OnJSON { + panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") + } + if options.Prefix != nil { + args = append(args, "PREFIX", len(options.Prefix)) + args = append(args, options.Prefix...) + } + if options.Filter != "" { + args = append(args, "FILTER", options.Filter) + } + if options.DefaultLanguage != "" { + args = append(args, "LANGUAGE", options.DefaultLanguage) + } + if options.LanguageField != "" { + args = append(args, "LANGUAGE_FIELD", options.LanguageField) + } + if options.Score > 0 { + args = append(args, "SCORE", options.Score) + } + if options.ScoreField != "" { + args = append(args, "SCORE_FIELD", options.ScoreField) + } + if options.PayloadField != "" { + args = append(args, "PAYLOAD_FIELD", options.PayloadField) + } + if options.MaxTextFields > 0 { + args = append(args, "MAXTEXTFIELDS", options.MaxTextFields) + } + if options.NoOffsets { + args = append(args, "NOOFFSETS") + } + if options.Temporary > 0 { + args = append(args, "TEMPORARY", options.Temporary) + } + if options.NoHL { + args = append(args, "NOHL") + } + if options.NoFields { + args = append(args, "NOFIELDS") + } + if options.NoFreqs { + args = append(args, "NOFREQS") + } + if options.StopWords != nil { + args = append(args, "STOPWORDS", len(options.StopWords)) + args = append(args, options.StopWords...) + } + if options.SkipInitalScan { + args = append(args, "SKIPINITIALSCAN") + } + } + if schema == nil { + panic("FT.CREATE: SCHEMA is required") + } + args = append(args, "SCHEMA") + for _, schema := range schema { + if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid { + panic("FT.CREATE: SCHEMA FieldName and FieldType are required") + } + args = append(args, schema.FieldName) + if schema.As != "" { + args = append(args, "AS", schema.As) + } + args = append(args, schema.FieldType.String()) + if schema.VectorArgs != nil { + if schema.FieldType != SearchFieldTypeVector { + panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") + } + if schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil { + panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive") + } + if schema.VectorArgs.FlatOptions != nil { + args = append(args, "FLAT") + if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" { + panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT") + } + flatArgs := []interface{}{ + "TYPE", schema.VectorArgs.FlatOptions.Type, + "DIM", schema.VectorArgs.FlatOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.FlatOptions.DistanceMetric, + } + if schema.VectorArgs.FlatOptions.InitialCapacity > 0 { + flatArgs = append(flatArgs, "INITIAL_CAP", schema.VectorArgs.FlatOptions.InitialCapacity) + } + if schema.VectorArgs.FlatOptions.BlockSize > 0 { + flatArgs = append(flatArgs, "BLOCK_SIZE", schema.VectorArgs.FlatOptions.BlockSize) + } + args = append(args, len(flatArgs)) + args = append(args, flatArgs...) + } + if schema.VectorArgs.HNSWOptions != nil { + args = append(args, "HNSW") + if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" { + panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW") + } + hnswArgs := []interface{}{ + "TYPE", schema.VectorArgs.HNSWOptions.Type, + "DIM", schema.VectorArgs.HNSWOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.HNSWOptions.DistanceMetric, + } + if schema.VectorArgs.HNSWOptions.InitialCapacity > 0 { + hnswArgs = append(hnswArgs, "INITIAL_CAP", schema.VectorArgs.HNSWOptions.InitialCapacity) + } + if schema.VectorArgs.HNSWOptions.MaxEdgesPerNode > 0 { + hnswArgs = append(hnswArgs, "M", schema.VectorArgs.HNSWOptions.MaxEdgesPerNode) + } + if schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode > 0 { + hnswArgs = append(hnswArgs, "EF_CONSTRUCTION", schema.VectorArgs.HNSWOptions.MaxAllowedEdgesPerNode) + } + if schema.VectorArgs.HNSWOptions.EFRunTime > 0 { + hnswArgs = append(hnswArgs, "EF_RUNTIME", schema.VectorArgs.HNSWOptions.EFRunTime) + } + if schema.VectorArgs.HNSWOptions.Epsilon > 0 { + hnswArgs = append(hnswArgs, "EPSILON", schema.VectorArgs.HNSWOptions.Epsilon) + } + args = append(args, len(hnswArgs)) + args = append(args, hnswArgs...) + } + } + if schema.GeoShapeFieldType != "" { + if schema.FieldType != SearchFieldTypeGeoShape { + panic("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType") + } + args = append(args, schema.GeoShapeFieldType) + } + if schema.NoStem { + args = append(args, "NOSTEM") + } + if schema.Sortable { + args = append(args, "SORTABLE") + } + if schema.UNF { + args = append(args, "UNF") + } + if schema.NoIndex { + args = append(args, "NOINDEX") + } + if schema.PhoneticMatcher != "" { + args = append(args, "PHONETIC", schema.PhoneticMatcher) + } + if schema.Weight > 0 { + args = append(args, "WEIGHT", schema.Weight) + } + if schema.Seperator != "" { + args = append(args, "SEPERATOR", schema.Seperator) + } + if schema.CaseSensitive { + args = append(args, "CASESENSITIVE") + } + if schema.WithSuffixtrie { + args = append(args, "WITHSUFFIXTRIE") + } + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTCursorDel - Deletes a cursor from an existing index. +// The 'index' parameter specifies the index from which to delete the cursor, and the 'cursorId' parameter specifies the ID of the cursor to delete. +// For more information, please refer to the Redis documentation: +// [FT.CURSOR DEL]: (https://redis.io/commands/ft.cursor-del/) +func (c cmdable) FTCursorDel(ctx context.Context, index string, cursorId int) *StatusCmd { + cmd := NewStatusCmd(ctx, "FT.CURSOR", "DEL", index, cursorId) + _ = c(ctx, cmd) + return cmd +} + +// FTCursorRead - Reads the next results from an existing cursor. +// The 'index' parameter specifies the index from which to read the cursor, the 'cursorId' parameter specifies the ID of the cursor to read, and the 'count' parameter specifies the number of results to read. +// For more information, please refer to the Redis documentation: +// [FT.CURSOR READ]: (https://redis.io/commands/ft.cursor-read/) +func (c cmdable) FTCursorRead(ctx context.Context, index string, cursorId int, count int) *MapStringInterfaceCmd { + args := []interface{}{"FT.CURSOR", "READ", index, cursorId} + if count > 0 { + args = append(args, "COUNT", count) + } + cmd := NewMapStringInterfaceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictAdd - Adds terms to a dictionary. +// The 'dict' parameter specifies the dictionary to which to add the terms, and the 'term' parameter specifies the terms to add. +// For more information, please refer to the Redis documentation: +// [FT.DICTADD]: (https://redis.io/commands/ft.dictadd/) +func (c cmdable) FTDictAdd(ctx context.Context, dict string, term ...interface{}) *IntCmd { + args := []interface{}{"FT.DICTADD", dict} + args = append(args, term...) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictDel - Deletes terms from a dictionary. +// The 'dict' parameter specifies the dictionary from which to delete the terms, and the 'term' parameter specifies the terms to delete. +// For more information, please refer to the Redis documentation: +// [FT.DICTDEL]: (https://redis.io/commands/ft.dictdel/) +func (c cmdable) FTDictDel(ctx context.Context, dict string, term ...interface{}) *IntCmd { + args := []interface{}{"FT.DICTDEL", dict} + args = append(args, term...) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDictDump - Returns all terms in the specified dictionary. +// The 'dict' parameter specifies the dictionary from which to return the terms. +// For more information, please refer to the Redis documentation: +// [FT.DICTDUMP]: (https://redis.io/commands/ft.dictdump/) +func (c cmdable) FTDictDump(ctx context.Context, dict string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT.DICTDUMP", dict) + _ = c(ctx, cmd) + return cmd +} + +// FTDropIndex - Deletes an index. +// The 'index' parameter specifies the index to delete. +// For more information, please refer to the Redis documentation: +// [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/) +func (c cmdable) FTDropIndex(ctx context.Context, index string) *StatusCmd { + args := []interface{}{"FT.DROPINDEX", index} + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTDropIndexWithArgs - Deletes an index with options. +// The 'index' parameter specifies the index to delete, and the 'options' parameter specifies the DeleteDocs option for docs deletion. +// For more information, please refer to the Redis documentation: +// [FT.DROPINDEX]: (https://redis.io/commands/ft.dropindex/) +func (c cmdable) FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd { + args := []interface{}{"FT.DROPINDEX", index} + if options != nil { + if options.DeleteDocs { + args = append(args, "DD") + } + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTExplain - Returns the execution plan for a complex query. +// The 'index' parameter specifies the index to query, and the 'query' parameter specifies the query string. +// For more information, please refer to the Redis documentation: +// [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/) +func (c cmdable) FTExplain(ctx context.Context, index string, query string) *StringCmd { + cmd := NewStringCmd(ctx, "FT.EXPLAIN", index, query) + _ = c(ctx, cmd) + return cmd +} + +// FTExplainWithArgs - Returns the execution plan for a complex query with options. +// The 'index' parameter specifies the index to query, the 'query' parameter specifies the query string, and the 'options' parameter specifies the Dialect for the query. +// For more information, please refer to the Redis documentation: +// [FT.EXPLAIN]: (https://redis.io/commands/ft.explain/) +func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd { + args := []interface{}{"FT.EXPLAIN", index, query} + if options.Dialect != "" { + args = append(args, "DIALECT", options.Dialect) + } + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTExplainCli - Returns the execution plan for a complex query. [Not Implemented] +// For more information, see https://redis.io/commands/ft.explaincli/ +func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { + panic("not implemented") +} + +func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { + var ftInfo FTInfoResult + // Manually parse each field from the map + if indexErrors, ok := data["Index Errors"].([]interface{}); ok { + ftInfo.IndexErrors = IndexErrors{ + IndexingFailures: internal.ToInteger(indexErrors[1]), + LastIndexingError: internal.ToString(indexErrors[3]), + LastIndexingErrorKey: internal.ToString(indexErrors[5]), + } + } + + if attributes, ok := data["attributes"].([]interface{}); ok { + for _, attr := range attributes { + if attrMap, ok := attr.([]interface{}); ok { + att := FTAttribute{} + for i := 0; i < len(attrMap); i++ { + if internal.ToLower(internal.ToString(attrMap[i])) == "attribute" { + att.Attribute = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "identifier" { + att.Identifier = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "type" { + att.Type = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "weight" { + att.Weight = internal.ToFloat(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "nostem" { + att.NoStem = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "sortable" { + att.Sortable = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "noindex" { + att.NoIndex = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "unf" { + att.UNF = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "phonetic" { + att.PhoneticMatcher = internal.ToString(attrMap[i+1]) + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "case_sensitive" { + att.CaseSensitive = true + continue + } + if internal.ToLower(internal.ToString(attrMap[i])) == "withsuffixtrie" { + att.WithSuffixtrie = true + continue + } + + } + ftInfo.Attributes = append(ftInfo.Attributes, att) + } + } + } + + ftInfo.BytesPerRecordAvg = internal.ToString(data["bytes_per_record_avg"]) + ftInfo.Cleaning = internal.ToInteger(data["cleaning"]) + + if cursorStats, ok := data["cursor_stats"].([]interface{}); ok { + ftInfo.CursorStats = CursorStats{ + GlobalIdle: internal.ToInteger(cursorStats[1]), + GlobalTotal: internal.ToInteger(cursorStats[3]), + IndexCapacity: internal.ToInteger(cursorStats[5]), + IndexTotal: internal.ToInteger(cursorStats[7]), + } + } + + if dialectStats, ok := data["dialect_stats"].([]interface{}); ok { + ftInfo.DialectStats = make(map[string]int) + for i := 0; i < len(dialectStats); i += 2 { + ftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1]) + } + } + + ftInfo.DocTableSizeMB = internal.ToFloat(data["doc_table_size_mb"]) + + if fieldStats, ok := data["field statistics"].([]interface{}); ok { + for _, stat := range fieldStats { + if statMap, ok := stat.([]interface{}); ok { + ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{ + Identifier: internal.ToString(statMap[1]), + Attribute: internal.ToString(statMap[3]), + IndexErrors: IndexErrors{ + IndexingFailures: internal.ToInteger(statMap[5].([]interface{})[1]), + LastIndexingError: internal.ToString(statMap[5].([]interface{})[3]), + LastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]), + }, + }) + } + } + } + + if gcStats, ok := data["gc_stats"].([]interface{}); ok { + ftInfo.GCStats = GCStats{} + for i := 0; i < len(gcStats); i += 2 { + if internal.ToLower(internal.ToString(gcStats[i])) == "bytes_collected" { + ftInfo.GCStats.BytesCollected = internal.ToInteger(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "total_ms_run" { + ftInfo.GCStats.TotalMsRun = internal.ToInteger(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "total_cycles" { + ftInfo.GCStats.TotalCycles = internal.ToInteger(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "average_cycle_time_ms" { + ftInfo.GCStats.AverageCycleTimeMs = internal.ToString(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "last_run_time_ms" { + ftInfo.GCStats.LastRunTimeMs = internal.ToInteger(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "gc_numeric_trees_missed" { + ftInfo.GCStats.GCNumericTreesMissed = internal.ToInteger(gcStats[i+1]) + continue + } + if internal.ToLower(internal.ToString(gcStats[i])) == "gc_blocks_denied" { + ftInfo.GCStats.GCBlocksDenied = internal.ToInteger(gcStats[i+1]) + continue + } + } + } + + ftInfo.GeoshapesSzMB = internal.ToFloat(data["geoshapes_sz_mb"]) + ftInfo.HashIndexingFailures = internal.ToInteger(data["hash_indexing_failures"]) + + if indexDef, ok := data["index_definition"].([]interface{}); ok { + ftInfo.IndexDefinition = IndexDefinition{ + KeyType: internal.ToString(indexDef[1]), + Prefixes: internal.ToStringSlice(indexDef[3]), + DefaultScore: internal.ToFloat(indexDef[5]), + } + } + + ftInfo.IndexName = internal.ToString(data["index_name"]) + ftInfo.IndexOptions = internal.ToStringSlice(data["index_options"].([]interface{})) + ftInfo.Indexing = internal.ToInteger(data["indexing"]) + ftInfo.InvertedSzMB = internal.ToFloat(data["inverted_sz_mb"]) + ftInfo.KeyTableSizeMB = internal.ToFloat(data["key_table_size_mb"]) + ftInfo.MaxDocID = internal.ToInteger(data["max_doc_id"]) + ftInfo.NumDocs = internal.ToInteger(data["num_docs"]) + ftInfo.NumRecords = internal.ToInteger(data["num_records"]) + ftInfo.NumTerms = internal.ToInteger(data["num_terms"]) + ftInfo.NumberOfUses = internal.ToInteger(data["number_of_uses"]) + ftInfo.OffsetBitsPerRecordAvg = internal.ToString(data["offset_bits_per_record_avg"]) + ftInfo.OffsetVectorsSzMB = internal.ToFloat(data["offset_vectors_sz_mb"]) + ftInfo.OffsetsPerTermAvg = internal.ToString(data["offsets_per_term_avg"]) + ftInfo.PercentIndexed = internal.ToFloat(data["percent_indexed"]) + ftInfo.RecordsPerDocAvg = internal.ToString(data["records_per_doc_avg"]) + ftInfo.SortableValuesSizeMB = internal.ToFloat(data["sortable_values_size_mb"]) + ftInfo.TagOverheadSzMB = internal.ToFloat(data["tag_overhead_sz_mb"]) + ftInfo.TextOverheadSzMB = internal.ToFloat(data["text_overhead_sz_mb"]) + ftInfo.TotalIndexMemorySzMB = internal.ToFloat(data["total_index_memory_sz_mb"]) + ftInfo.TotalIndexingTime = internal.ToInteger(data["total_indexing_time"]) + ftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data["total_inverted_index_blocks"]) + ftInfo.VectorIndexSzMB = internal.ToFloat(data["vector_index_sz_mb"]) + + return ftInfo, nil +} + +type FTInfoCmd struct { + baseCmd + val FTInfoResult +} + +func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd { + return &FTInfoCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FTInfoCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTInfoCmd) SetVal(val FTInfoResult) { + cmd.val = val +} + +func (cmd *FTInfoCmd) Result() (FTInfoResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTInfoCmd) Val() FTInfoResult { + return cmd.val +} + +func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + + data := make(map[string]interface{}, n) + for i := 0; i < n; i++ { + k, err := rd.ReadString() + if err != nil { + return err + } + v, err := rd.ReadReply() + if err != nil { + if err == Nil { + data[k] = Nil + continue + } + if err, ok := err.(proto.RedisError); ok { + data[k] = err + continue + } + return err + } + data[k] = v + } + cmd.val, err = parseFTInfo(data) + if err != nil { + cmd.err = err + } + + return nil +} + +// FTInfo - Retrieves information about an index. +// The 'index' parameter specifies the index to retrieve information about. +// For more information, please refer to the Redis documentation: +// [FT.INFO]: (https://redis.io/commands/ft.info/) +func (c cmdable) FTInfo(ctx context.Context, index string) *FTInfoCmd { + cmd := newFTInfoCmd(ctx, "FT.INFO", index) + _ = c(ctx, cmd) + return cmd +} + +// FTSpellCheck - Checks a query string for spelling errors. +// For more details about spellcheck query please follow: +// https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/ +// For more information, please refer to the Redis documentation: +// [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/) +func (c cmdable) FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd { + args := []interface{}{"FT.SPELLCHECK", index, query} + cmd := newFTSpellCheckCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTSpellCheckWithArgs - Checks a query string for spelling errors with additional options. +// For more details about spellcheck query please follow: +// https://redis.io/docs/interact/search-and-query/advanced-concepts/spellcheck/ +// For more information, please refer to the Redis documentation: +// [FT.SPELLCHECK]: (https://redis.io/commands/ft.spellcheck/) +func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd { + args := []interface{}{"FT.SPELLCHECK", index, query} + if options != nil { + if options.Distance > 0 { + args = append(args, "DISTANCE", options.Distance) + } + if options.Terms != nil { + args = append(args, "TERMS", options.Terms.Inclusion, options.Terms.Dictionary) + args = append(args, options.Terms.Terms...) + } + if options.Dialect > 0 { + args = append(args, "DIALECT", options.Dialect) + } + } + cmd := newFTSpellCheckCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type FTSpellCheckCmd struct { + baseCmd + val []SpellCheckResult +} + +func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd { + return &FTSpellCheckCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FTSpellCheckCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSpellCheckCmd) SetVal(val []SpellCheckResult) { + cmd.val = val +} + +func (cmd *FTSpellCheckCmd) Result() ([]SpellCheckResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult { + return cmd.val +} + +func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = parseFTSpellCheck(data) + if err != nil { + cmd.err = err + } + return nil +} + +func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) { + results := make([]SpellCheckResult, 0, len(data)) + + for _, termData := range data { + termInfo, ok := termData.([]interface{}) + if !ok || len(termInfo) != 3 { + return nil, fmt.Errorf("invalid term format") + } + + term, ok := termInfo[1].(string) + if !ok { + return nil, fmt.Errorf("invalid term format") + } + + suggestionsData, ok := termInfo[2].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid suggestions format") + } + + suggestions := make([]SpellCheckSuggestion, 0, len(suggestionsData)) + for _, suggestionData := range suggestionsData { + suggestionInfo, ok := suggestionData.([]interface{}) + if !ok || len(suggestionInfo) != 2 { + return nil, fmt.Errorf("invalid suggestion format") + } + + scoreStr, ok := suggestionInfo[0].(string) + if !ok { + return nil, fmt.Errorf("invalid suggestion score format") + } + score, err := strconv.ParseFloat(scoreStr, 64) + if err != nil { + return nil, fmt.Errorf("invalid suggestion score value") + } + + suggestion, ok := suggestionInfo[1].(string) + if !ok { + return nil, fmt.Errorf("invalid suggestion format") + } + + suggestions = append(suggestions, SpellCheckSuggestion{ + Score: score, + Suggestion: suggestion, + }) + } + + results = append(results, SpellCheckResult{ + Term: term, + Suggestions: suggestions, + }) + } + + return results, nil +} + +func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) { + if len(data) < 1 { + return FTSearchResult{}, fmt.Errorf("unexpected search result format") + } + + total, ok := data[0].(int64) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid total results format") + } + + var results []Document + for i := 1; i < len(data); { + docID, ok := data[i].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid document ID format") + } + + doc := Document{ + ID: docID, + Fields: make(map[string]string), + } + i++ + + if noContent { + results = append(results, doc) + continue + } + + if withScores && i < len(data) { + if scoreStr, ok := data[i].(string); ok { + score, err := strconv.ParseFloat(scoreStr, 64) + if err != nil { + return FTSearchResult{}, fmt.Errorf("invalid score format") + } + doc.Score = &score + i++ + } + } + + if withPayloads && i < len(data) { + if payload, ok := data[i].(string); ok { + doc.Payload = &payload + i++ + } + } + + if withSortKeys && i < len(data) { + if sortKey, ok := data[i].(string); ok { + doc.SortKey = &sortKey + i++ + } + } + + if i < len(data) { + fields, ok := data[i].([]interface{}) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } + + for j := 0; j < len(fields); j += 2 { + key, ok := fields[j].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid field key format") + } + value, ok := fields[j+1].(string) + if !ok { + return FTSearchResult{}, fmt.Errorf("invalid field value format") + } + doc.Fields[key] = value + } + i++ + } + + results = append(results, doc) + } + return FTSearchResult{ + Total: int(total), + Docs: results, + }, nil +} + +type FTSearchCmd struct { + baseCmd + val FTSearchResult + options *FTSearchOptions +} + +func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd { + return &FTSearchCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + options: options, + } +} + +func (cmd *FTSearchCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSearchCmd) SetVal(val FTSearchResult) { + cmd.val = val +} + +func (cmd *FTSearchCmd) Result() (FTSearchResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSearchCmd) Val() FTSearchResult { + return cmd.val +} + +func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { + data, err := rd.ReadSlice() + if err != nil { + cmd.err = err + return nil + } + cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + if err != nil { + cmd.err = err + } + return nil +} + +// FTSearch - Executes a search query on an index. +// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. +// For more information, please refer to the Redis documentation: +// [FT.SEARCH]: (https://redis.io/commands/ft.search/) +func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd { + args := []interface{}{"FT.SEARCH", index, query} + cmd := newFTSearchCmd(ctx, &FTSearchOptions{}, args...) + _ = c(ctx, cmd) + return cmd +} + +type SearchQuery []interface{} + +func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { + queryArgs := []interface{}{query} + if options != nil { + if options.NoContent { + queryArgs = append(queryArgs, "NOCONTENT") + } + if options.Verbatim { + queryArgs = append(queryArgs, "VERBATIM") + } + if options.NoStopWrods { + queryArgs = append(queryArgs, "NOSTOPWORDS") + } + if options.WithScores { + queryArgs = append(queryArgs, "WITHSCORES") + } + if options.WithPayloads { + queryArgs = append(queryArgs, "WITHPAYLOADS") + } + if options.WithSortKeys { + queryArgs = append(queryArgs, "WITHSORTKEYS") + } + if options.Filters != nil { + for _, filter := range options.Filters { + queryArgs = append(queryArgs, "FILTER", filter.FieldName, filter.Min, filter.Max) + } + } + if options.GeoFilter != nil { + for _, geoFilter := range options.GeoFilter { + queryArgs = append(queryArgs, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit) + } + } + if options.InKeys != nil { + queryArgs = append(queryArgs, "INKEYS", len(options.InKeys)) + queryArgs = append(queryArgs, options.InKeys...) + } + if options.InFields != nil { + queryArgs = append(queryArgs, "INFIELDS", len(options.InFields)) + queryArgs = append(queryArgs, options.InFields...) + } + if options.Return != nil { + queryArgs = append(queryArgs, "RETURN") + queryArgsReturn := []interface{}{} + for _, ret := range options.Return { + queryArgsReturn = append(queryArgsReturn, ret.FieldName) + if ret.As != "" { + queryArgsReturn = append(queryArgsReturn, "AS", ret.As) + } + } + queryArgs = append(queryArgs, len(queryArgsReturn)) + queryArgs = append(queryArgs, queryArgsReturn...) + } + if options.Slop > 0 { + queryArgs = append(queryArgs, "SLOP", options.Slop) + } + if options.Timeout > 0 { + queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) + } + if options.InOrder { + queryArgs = append(queryArgs, "INORDER") + } + if options.Language != "" { + queryArgs = append(queryArgs, "LANGUAGE", options.Language) + } + if options.Expander != "" { + queryArgs = append(queryArgs, "EXPANDER", options.Expander) + } + if options.Scorer != "" { + queryArgs = append(queryArgs, "SCORER", options.Scorer) + } + if options.ExplainScore { + queryArgs = append(queryArgs, "EXPLAINSCORE") + } + if options.Payload != "" { + queryArgs = append(queryArgs, "PAYLOAD", options.Payload) + } + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + for _, sortBy := range options.SortBy { + queryArgs = append(queryArgs, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.SEARCH: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + queryArgs = append(queryArgs, "ASC") + } + if sortBy.Desc { + queryArgs = append(queryArgs, "DESC") + } + } + if options.SortByWithCount { + queryArgs = append(queryArgs, "WITHCOUT") + } + } + if options.LimitOffset >= 0 && options.Limit > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) + } + if options.Params != nil { + queryArgs = append(queryArgs, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + queryArgs = append(queryArgs, key, value) + } + } + if options.DialectVersion > 0 { + queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } + } + return queryArgs +} + +// FTSearchWithArgs - Executes a search query on an index with additional options. +// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, +// and the 'options' parameter specifies additional options for the search. +// For more information, please refer to the Redis documentation: +// [FT.SEARCH]: (https://redis.io/commands/ft.search/) +func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd { + args := []interface{}{"FT.SEARCH", index, query} + if options != nil { + if options.NoContent { + args = append(args, "NOCONTENT") + } + if options.Verbatim { + args = append(args, "VERBATIM") + } + if options.NoStopWrods { + args = append(args, "NOSTOPWORDS") + } + if options.WithScores { + args = append(args, "WITHSCORES") + } + if options.WithPayloads { + args = append(args, "WITHPAYLOADS") + } + if options.WithSortKeys { + args = append(args, "WITHSORTKEYS") + } + if options.Filters != nil { + for _, filter := range options.Filters { + args = append(args, "FILTER", filter.FieldName, filter.Min, filter.Max) + } + } + if options.GeoFilter != nil { + for _, geoFilter := range options.GeoFilter { + args = append(args, "GEOFILTER", geoFilter.FieldName, geoFilter.Longitude, geoFilter.Latitude, geoFilter.Radius, geoFilter.Unit) + } + } + if options.InKeys != nil { + args = append(args, "INKEYS", len(options.InKeys)) + args = append(args, options.InKeys...) + } + if options.InFields != nil { + args = append(args, "INFIELDS", len(options.InFields)) + args = append(args, options.InFields...) + } + if options.Return != nil { + args = append(args, "RETURN") + argsReturn := []interface{}{} + for _, ret := range options.Return { + argsReturn = append(argsReturn, ret.FieldName) + if ret.As != "" { + argsReturn = append(argsReturn, "AS", ret.As) + } + } + args = append(args, len(argsReturn)) + args = append(args, argsReturn...) + } + if options.Slop > 0 { + args = append(args, "SLOP", options.Slop) + } + if options.Timeout > 0 { + args = append(args, "TIMEOUT", options.Timeout) + } + if options.InOrder { + args = append(args, "INORDER") + } + if options.Language != "" { + args = append(args, "LANGUAGE", options.Language) + } + if options.Expander != "" { + args = append(args, "EXPANDER", options.Expander) + } + if options.Scorer != "" { + args = append(args, "SCORER", options.Scorer) + } + if options.ExplainScore { + args = append(args, "EXPLAINSCORE") + } + if options.Payload != "" { + args = append(args, "PAYLOAD", options.Payload) + } + if options.SortBy != nil { + args = append(args, "SORTBY") + for _, sortBy := range options.SortBy { + args = append(args, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + panic("FT.SEARCH: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + args = append(args, "ASC") + } + if sortBy.Desc { + args = append(args, "DESC") + } + } + if options.SortByWithCount { + args = append(args, "WITHCOUT") + } + } + if options.LimitOffset >= 0 && options.Limit > 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) + } + if options.Params != nil { + args = append(args, "PARAMS", len(options.Params)*2) + for key, value := range options.Params { + args = append(args, key, value) + } + } + if options.DialectVersion > 0 { + args = append(args, "DIALECT", options.DialectVersion) + } + } + cmd := newFTSearchCmd(ctx, options, args...) + _ = c(ctx, cmd) + return cmd +} + +func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd { + return &FTSynDumpCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *FTSynDumpCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *FTSynDumpCmd) SetVal(val []FTSynDumpResult) { + cmd.val = val +} + +func (cmd *FTSynDumpCmd) Val() []FTSynDumpResult { + return cmd.val +} + +func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) { + return cmd.val, cmd.err +} + +func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { + termSynonymPairs, err := rd.ReadSlice() + if err != nil { + return err + } + + var results []FTSynDumpResult + for i := 0; i < len(termSynonymPairs); i += 2 { + term, ok := termSynonymPairs[i].(string) + if !ok { + return fmt.Errorf("invalid term format") + } + + synonyms, ok := termSynonymPairs[i+1].([]interface{}) + if !ok { + return fmt.Errorf("invalid synonyms format") + } + + synonymList := make([]string, len(synonyms)) + for j, syn := range synonyms { + synonym, ok := syn.(string) + if !ok { + return fmt.Errorf("invalid synonym format") + } + synonymList[j] = synonym + } + + results = append(results, FTSynDumpResult{ + Term: term, + Synonyms: synonymList, + }) + } + + cmd.val = results + return nil +} + +// FTSynDump - Dumps the contents of a synonym group. +// The 'index' parameter specifies the index to dump. +// For more information, please refer to the Redis documentation: +// [FT.SYNDUMP]: (https://redis.io/commands/ft.syndump/) +func (c cmdable) FTSynDump(ctx context.Context, index string) *FTSynDumpCmd { + cmd := NewFTSynDumpCmd(ctx, "FT.SYNDUMP", index) + _ = c(ctx, cmd) + return cmd +} + +// FTSynUpdate - Creates or updates a synonym group with additional terms. +// The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, and the 'terms' parameter specifies the additional terms. +// For more information, please refer to the Redis documentation: +// [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/) +func (c cmdable) FTSynUpdate(ctx context.Context, index string, synGroupId interface{}, terms []interface{}) *StatusCmd { + args := []interface{}{"FT.SYNUPDATE", index, synGroupId} + args = append(args, terms...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTSynUpdateWithArgs - Creates or updates a synonym group with additional terms and options. +// The 'index' parameter specifies the index to update, the 'synGroupId' parameter specifies the synonym group id, the 'options' parameter specifies additional options for the update, and the 'terms' parameter specifies the additional terms. +// For more information, please refer to the Redis documentation: +// [FT.SYNUPDATE]: (https://redis.io/commands/ft.synupdate/) +func (c cmdable) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId interface{}, options *FTSynUpdateOptions, terms []interface{}) *StatusCmd { + args := []interface{}{"FT.SYNUPDATE", index, synGroupId} + if options.SkipInitialScan { + args = append(args, "SKIPINITIALSCAN") + } + args = append(args, terms...) + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// FTTagVals - Returns all distinct values indexed in a tag field. +// The 'index' parameter specifies the index to check, and the 'field' parameter specifies the tag field to retrieve values from. +// For more information, please refer to the Redis documentation: +// [FT.TAGVALS]: (https://redis.io/commands/ft.tagvals/) +func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "FT.TAGVALS", index, field) + _ = c(ctx, cmd) + return cmd +} + +// type FTProfileResult struct { +// Results []interface{} +// Profile ProfileDetails +// } + +// type ProfileDetails struct { +// TotalProfileTime string +// ParsingTime string +// PipelineCreationTime string +// Warning string +// IteratorsProfile []IteratorProfile +// ResultProcessorsProfile []ResultProcessorProfile +// } + +// type IteratorProfile struct { +// Type string +// QueryType string +// Time interface{} +// Counter int +// Term string +// Size int +// ChildIterators []IteratorProfile +// } + +// type ResultProcessorProfile struct { +// Type string +// Time interface{} +// Counter int +// } + +// func parseFTProfileResult(data []interface{}) (FTProfileResult, error) { +// var result FTProfileResult +// if len(data) < 2 { +// return result, fmt.Errorf("unexpected data length") +// } + +// // Parse results +// result.Results = data[0].([]interface{}) + +// // Parse profile details +// profileData := data[1].([]interface{}) +// profileDetails := ProfileDetails{} +// for i := 0; i < len(profileData); i += 2 { +// switch profileData[i].(string) { +// case "Total profile time": +// profileDetails.TotalProfileTime = profileData[i+1].(string) +// case "Parsing time": +// profileDetails.ParsingTime = profileData[i+1].(string) +// case "Pipeline creation time": +// profileDetails.PipelineCreationTime = profileData[i+1].(string) +// case "Warning": +// profileDetails.Warning = profileData[i+1].(string) +// case "Iterators profile": +// profileDetails.IteratorsProfile = parseIteratorsProfile(profileData[i+1].([]interface{})) +// case "Result processors profile": +// profileDetails.ResultProcessorsProfile = parseResultProcessorsProfile(profileData[i+1].([]interface{})) +// } +// } + +// result.Profile = profileDetails +// return result, nil +// } + +// func parseIteratorsProfile(data []interface{}) []IteratorProfile { +// var iterators []IteratorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// iterator := IteratorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// iterator.Type = profile[i+1].(string) +// case "Query type": +// iterator.QueryType = profile[i+1].(string) +// case "Time": +// iterator.Time = profile[i+1] +// case "Counter": +// iterator.Counter = int(profile[i+1].(int64)) +// case "Term": +// iterator.Term = profile[i+1].(string) +// case "Size": +// iterator.Size = int(profile[i+1].(int64)) +// case "Child iterators": +// iterator.ChildIterators = parseChildIteratorsProfile(profile[i+1].([]interface{})) +// } +// } +// iterators = append(iterators, iterator) +// } +// return iterators +// } + +// func parseChildIteratorsProfile(data []interface{}) []IteratorProfile { +// var iterators []IteratorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// iterator := IteratorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// iterator.Type = profile[i+1].(string) +// case "Query type": +// iterator.QueryType = profile[i+1].(string) +// case "Time": +// iterator.Time = profile[i+1] +// case "Counter": +// iterator.Counter = int(profile[i+1].(int64)) +// case "Term": +// iterator.Term = profile[i+1].(string) +// case "Size": +// iterator.Size = int(profile[i+1].(int64)) +// } +// } +// iterators = append(iterators, iterator) +// } +// return iterators +// } + +// func parseResultProcessorsProfile(data []interface{}) []ResultProcessorProfile { +// var processors []ResultProcessorProfile +// for _, item := range data { +// profile := item.([]interface{}) +// processor := ResultProcessorProfile{} +// for i := 0; i < len(profile); i += 2 { +// switch profile[i].(string) { +// case "Type": +// processor.Type = profile[i+1].(string) +// case "Time": +// processor.Time = profile[i+1] +// case "Counter": +// processor.Counter = int(profile[i+1].(int64)) +// } +// } +// processors = append(processors, processor) +// } +// return processors +// } + +// func NewFTProfileCmd(ctx context.Context, args ...interface{}) *FTProfileCmd { +// return &FTProfileCmd{ +// baseCmd: baseCmd{ +// ctx: ctx, +// args: args, +// }, +// } +// } + +// type FTProfileCmd struct { +// baseCmd +// val FTProfileResult +// } + +// func (cmd *FTProfileCmd) String() string { +// return cmdString(cmd, cmd.val) +// } + +// func (cmd *FTProfileCmd) SetVal(val FTProfileResult) { +// cmd.val = val +// } + +// func (cmd *FTProfileCmd) Result() (FTProfileResult, error) { +// return cmd.val, cmd.err +// } + +// func (cmd *FTProfileCmd) Val() FTProfileResult { +// return cmd.val +// } + +// func (cmd *FTProfileCmd) readReply(rd *proto.Reader) (err error) { +// data, err := rd.ReadSlice() +// if err != nil { +// return err +// } +// cmd.val, err = parseFTProfileResult(data) +// if err != nil { +// cmd.err = err +// } +// return nil +// } + +// // FTProfile - Executes a search query and returns a profile of how the query was processed. +// // The 'index' parameter specifies the index to search, the 'limited' parameter specifies whether to limit the results, +// // and the 'query' parameter specifies the search / aggreagte query. Please notice that you must either pass a SearchQuery or an AggregateQuery. +// // For more information, please refer to the Redis documentation: +// // [FT.PROFILE]: (https://redis.io/commands/ft.profile/) +// func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, query interface{}) *FTProfileCmd { +// queryType := "" +// var argsQuery []interface{} + +// switch v := query.(type) { +// case AggregateQuery: +// queryType = "AGGREGATE" +// argsQuery = v +// case SearchQuery: +// queryType = "SEARCH" +// argsQuery = v +// default: +// panic("FT.PROFILE: query must be either AggregateQuery or SearchQuery") +// } + +// args := []interface{}{"FT.PROFILE", index, queryType} + +// if limited { +// args = append(args, "LIMITED") +// } +// args = append(args, "QUERY") +// args = append(args, argsQuery...) + +// cmd := NewFTProfileCmd(ctx, args...) +// _ = c(ctx, cmd) +// return cmd +// } diff --git a/search_test.go b/search_test.go new file mode 100644 index 0000000000..60888ef5c2 --- /dev/null +++ b/search_test.go @@ -0,0 +1,1136 @@ +package redis_test + +import ( + "context" + "time" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +func WaitForIndexing(c *redis.Client, index string) { + for { + res, err := c.FTInfo(context.Background(), index).Result() + Expect(err).NotTo(HaveOccurred()) + if c.Options().Protocol == 2 { + if res.Indexing == 0 { + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +var _ = Describe("RediSearch commands", Label("search"), func() { + ctx := context.TODO() + var client *redis.Client + + BeforeEach(func() { + client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("should FTCreate and FTSearch WithScores", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + res, err := client.FTSearchWithArgs(ctx, "txt", "foo ~bar", &redis.FTSearchOptions{WithScores: true}).Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(int64(2))) + for _, doc := range res.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + Expect(doc.ID).To(Or(Equal("doc1"), Equal("doc2"))) + } + }) + + It("should FTCreate and FTSearch stopwords", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "hello world") + res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(0))) + res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(1))) + }) + + It("should FTCreate and FTSearch filters", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}, &redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458") + client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2") + res1, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: 2}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc2")) + res2, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{Filters: []redis.FTSearchFilter{{FieldName: "num", Min: 0, Max: "+inf"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc1")) + // Test Geo filter + geoFilter1 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 10, Unit: "km"} + geoFilter2 := redis.FTSearchGeoFilter{FieldName: "loc", Longitude: -0.44, Latitude: 51.45, Radius: 100, Unit: "km"} + res3, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter1}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(BeEquivalentTo(int64(1))) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc1")) + res4, err := client.FTSearchWithArgs(ctx, "txt", "foo", &redis.FTSearchOptions{GeoFilter: []redis.FTSearchGeoFilter{geoFilter2}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeEquivalentTo(int64(2))) + docs := []interface{}{res4.Docs[0].ID, res4.Docs[1].ID} + Expect(docs).To(ContainElement("doc1")) + Expect(docs).To(ContainElement("doc2")) + + }) + + It("should FTCreate and FTSearch sortby", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "num", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, &redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "num") + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1) + client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2) + client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3) + + sortBy1 := redis.FTSearchSortBy{FieldName: "num", Asc: true} + sortBy2 := redis.FTSearchSortBy{FieldName: "num", Desc: true} + res1, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy1}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(3))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3")) + + res2, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(3))) + Expect(res2.Docs[2].ID).To(BeEquivalentTo("doc1")) + Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3")) + + }) + + It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}, &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis") + res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + + }) + + It("should FTCreate NoIndex", Label("search", "ftcreate", "ftsearch"), func() { + text1 := &redis.FieldSchema{FieldName: "field", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "text", FieldType: redis.SearchFieldTypeText, NoIndex: true, Sortable: true} + num := &redis.FieldSchema{FieldName: "numeric", FieldType: redis.SearchFieldTypeNumeric, NoIndex: true, Sortable: true} + geo := &redis.FieldSchema{FieldName: "geo", FieldType: redis.SearchFieldTypeGeo, NoIndex: true, Sortable: true} + tag := &redis.FieldSchema{FieldName: "tag", FieldType: redis.SearchFieldTypeTag, NoIndex: true, Sortable: true} + val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, text1, text2, num, geo, tag).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx") + client.HSet(ctx, "doc1", "field", "aaa", "text", "1", "numeric", 1, "geo", "1,1", "tag", "1") + client.HSet(ctx, "doc2", "field", "aab", "text", "2", "numeric", 2, "geo", "2,2", "tag", "2") + res1, err := client.FTSearch(ctx, "idx", "@text:aa*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(0))) + res2, err := client.FTSearch(ctx, "idx", "@field:aa*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + res3, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Desc: true}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(BeEquivalentTo(int64(2))) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("doc2")) + res4, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "text", Asc: true}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeEquivalentTo(int64(2))) + Expect(res4.Docs[0].ID).To(BeEquivalentTo("doc1")) + res5, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "numeric", Asc: true}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res5.Docs[0].ID).To(BeEquivalentTo("doc1")) + res6, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "geo", Asc: true}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res6.Docs[0].ID).To(BeEquivalentTo("doc1")) + res7, err := client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{SortBy: []redis.FTSearchSortBy{{FieldName: "tag", Asc: true}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res7.Docs[0].ID).To(BeEquivalentTo("doc1")) + + }) + + It("should FTExplain", Label("search", "ftexplain"), func() { + text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText} + text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1).ToNot(BeEmpty()) + + }) + + It("should FTAlias", Label("search", "ftexplain"), func() { + text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} + val1, err := client.FTCreate(ctx, "testAlias", &redis.FTCreateOptions{Prefix: []interface{}{"index1:"}}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val1).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "testAlias") + val2, err := client.FTCreate(ctx, "testAlias2", &redis.FTCreateOptions{Prefix: []interface{}{"index2:"}}, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val2).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "testAlias2") + + client.HSet(ctx, "index1:lonestar", "name", "lonestar") + client.HSet(ctx, "index2:yogurt", "name", "yogurt") + + res1, err := client.FTSearch(ctx, "testAlias", "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar")) + + aliasAddRes, err := client.FTAliasAdd(ctx, "testAlias", "mj23").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasAddRes).To(BeEquivalentTo("OK")) + + res1, err = client.FTSearch(ctx, "mj23", "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("index1:lonestar")) + + aliasUpdateRes, err := client.FTAliasUpdate(ctx, "testAlias2", "kb24").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasUpdateRes).To(BeEquivalentTo("OK")) + + res3, err := client.FTSearch(ctx, "kb24", "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Docs[0].ID).To(BeEquivalentTo("index2:yogurt")) + + aliasDelRes, err := client.FTAliasDel(ctx, "mj23").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(aliasDelRes).To(BeEquivalentTo("OK")) + + }) + + It("should FTCreate and FTSearch textfield, sortable and nostem ", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resInfo, err := client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resInfo.Attributes[0].Sortable).To(BeTrue()) + Expect(resInfo.Attributes[0].NoStem).To(BeTrue()) + + }) + + It("should FTAlter", Label("search", "ftcreate", "ftsearch", "ftalter"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resAlter, err := client.FTAlter(ctx, "idx1", false, []interface{}{"body", redis.SearchFieldTypeText.String()}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAlter).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "doc1", "title", "MyTitle", "body", "Some content only in the body") + res1, err := client.FTSearch(ctx, "idx1", "only in the body").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + + }) + + It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() { + text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text") + client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum") + + resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck[0].Suggestions[0].Suggestion).To(BeEquivalentTo("important")) + + resSpellCheck2, err := client.FTSpellCheck(ctx, "idx1", "contnt").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck2[0].Suggestions[0].Suggestion).To(BeEquivalentTo("content")) + + // test spellcheck with Levenshtein distance + resSpellCheck3, err := client.FTSpellCheck(ctx, "idx1", "vlis").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck3[0].Term).To(BeEquivalentTo("vlis")) + + resSpellCheck4, err := client.FTSpellCheckWithArgs(ctx, "idx1", "vlis", &redis.FTSpellCheckOptions{Distance: 2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck4[0].Suggestions[0].Suggestion).To(BeEquivalentTo("valid")) + + // test spellcheck include + resDictAdd, err := client.FTDictAdd(ctx, "dict", "lore", "lorem", "lorm").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictAdd).To(BeEquivalentTo(3)) + terms := &redis.FTSpellCheckTerms{Inclusion: "INCLUDE", Dictionary: "dict"} + resSpellCheck5, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms}).Result() + Expect(err).NotTo(HaveOccurred()) + lorm := resSpellCheck5[0].Suggestions + Expect(len(lorm)).To(BeEquivalentTo(3)) + Expect(lorm[0].Score).To(BeEquivalentTo(0.5)) + Expect(lorm[1].Score).To(BeEquivalentTo(0)) + Expect(lorm[2].Score).To(BeEquivalentTo(0)) + + terms2 := &redis.FTSpellCheckTerms{Inclusion: "EXCLUDE", Dictionary: "dict"} + resSpellCheck6, err := client.FTSpellCheckWithArgs(ctx, "idx1", "lorm", &redis.FTSpellCheckOptions{Terms: terms2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSpellCheck6).To(BeEmpty()) + }) + + It("should FTDict opreations", Label("search", "ftdictdump", "ftdictdel", "ftdictadd"), func() { + text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resDictAdd, err := client.FTDictAdd(ctx, "custom_dict", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictAdd).To(BeEquivalentTo(3)) + + resDictDel, err := client.FTDictDel(ctx, "custom_dict", "item2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDel).To(BeEquivalentTo(1)) + + resDictDump, err := client.FTDictDump(ctx, "custom_dict").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDump).To(BeEquivalentTo([]string{"item1", "item3"})) + + resDictDel2, err := client.FTDictDel(ctx, "custom_dict", "item1", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDictDel2).To(BeEquivalentTo(2)) + }) + + It("should FTSearch phonetic matcher", Label("search", "ftsearch"), func() { + text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "name", "Jon") + client.HSet(ctx, "doc2", "name", "John") + + res1, err := client.FTSearch(ctx, "idx1", "Jon").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon")) + + client.FlushDB(ctx) + text2 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText, PhoneticMatcher: "dm:en"} + val2, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val2).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "name", "Jon") + client.HSet(ctx, "doc2", "name", "John") + + res2, err := client.FTSearch(ctx, "idx1", "Jon").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + names := []interface{}{res2.Docs[0].Fields["name"], res2.Docs[1].Fields["name"]} + Expect(names).To(ContainElement("Jon")) + Expect(names).To(ContainElement("John")) + }) + + It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") + client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.") + + res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.22471909420069797)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0))) + }) + + It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + val, err := client.FTConfigSet(ctx, "TIMEOUT", "100").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res, err := client.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res["TIMEOUT"]).To(BeEquivalentTo("100")) + + res, err = client.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + + }) + + It("should FTAggregate GroupBy ", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText} + text3 := &redis.FieldSchema{FieldName: "parent", FieldType: redis.SearchFieldTypeText} + num := &redis.FieldSchema{FieldName: "random_num", FieldType: redis.SearchFieldTypeNumeric} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2, text3, num).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "search", "title", "RediSearch", + "body", "Redisearch impements a search engine on top of redis", + "parent", "redis", + "random_num", 10) + client.HSet(ctx, "ai", "title", "RedisAI", + "body", "RedisAI executes Deep Learning/Machine Learning models and managing their data.", + "parent", "redis", + "random_num", 3) + client.HSet(ctx, "json", "title", "RedisJson", + "body", "RedisJSON implements ECMA-404 The JSON Data Interchange Standard as a native data type.", + "parent", "redis", + "random_num", 8) + + reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount} + options := &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliascount"]).To(BeEquivalentTo("3")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchCountDistinct, Args: []interface{}{"@title"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliascount_distincttitle"]).To(BeEquivalentTo("3")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchSum, Args: []interface{}{"@random_num"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliassumrandom_num"]).To(BeEquivalentTo("21")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchMin, Args: []interface{}{"@random_num"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasminrandom_num"]).To(BeEquivalentTo("3")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchMax, Args: []interface{}{"@random_num"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasmaxrandom_num"]).To(BeEquivalentTo("10")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchAvg, Args: []interface{}{"@random_num"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasavgrandom_num"]).To(BeEquivalentTo("7")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchStdDev, Args: []interface{}{"@random_num"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasstddevrandom_num"]).To(BeEquivalentTo("3.60555127546")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchQuantile, Args: []interface{}{"@random_num", 0.5}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliasquantilerandom_num,0.5"]).To(BeEquivalentTo("8")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchToList, Args: []interface{}{"@title"}} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["__generated_aliastolisttitle"]).To(ContainElements("RediSearch", "RedisAI", "RedisJson")) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchFirstValue, Args: []interface{}{"@title"}, As: "first"} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["first"]).To(Or(BeEquivalentTo("RediSearch"), BeEquivalentTo("RedisAI"), BeEquivalentTo("RedisJson"))) + + reducer = redis.FTAggregateReducer{Reducer: redis.SearchRandomSample, Args: []interface{}{"@title", 2}, As: "random"} + options = &redis.FTAggregateOptions{GroupBy: []redis.FTAggregateGroupBy{{Fields: []interface{}{"@parent"}, Reduce: []redis.FTAggregateReducer{reducer}}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "redis", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["parent"]).To(BeEquivalentTo("redis")) + Expect(res.Rows[0].Fields["random"]).To(Or( + ContainElement("RediSearch"), + ContainElement("RedisAI"), + ContainElement("RedisJson"), + )) + + }) + + It("should FTAggregate sort and limit", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "t1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "t2", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "t1", "a", "t2", "b") + client.HSet(ctx, "doc2", "t1", "b", "t2", "a") + + options := &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t2", Asc: true}, {FieldName: "@t1", Desc: true}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) + Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("a")) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("a")) + Expect(res.Rows[1].Fields["t2"]).To(BeEquivalentTo("b")) + + options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) + Expect(res.Rows[1].Fields["t1"]).To(BeEquivalentTo("b")) + + options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, SortByMax: 1} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) + + options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 1} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) + }) + + It("should FTAggregate load ", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "t1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "t2", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "t1", "hello", "t2", "world") + + options := &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) + + options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + + options = &redis.FTAggregateOptions{LoadAll: true} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) + Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + }) + + It("should FTAggregate apply", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} + num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984") + client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016") + + options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + Expect(res.Rows[1].Fields["CreatedDateTimeUTC"]).To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + + }) + + It("should FTAggregate filter", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText, Sortable: true} + num1 := &redis.FieldSchema{FieldName: "age", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "name", "bar", "age", "25") + client.HSet(ctx, "doc2", "name", "foo", "age", "19") + + for _, dlc := range []int{1, 2} { + options := &redis.FTAggregateOptions{Filter: "@name=='foo' && @age < 20", DialectVersion: dlc} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Or(BeEquivalentTo(2), BeEquivalentTo(1))) + Expect(res.Rows[0].Fields["name"]).To(BeEquivalentTo("foo")) + + options = &redis.FTAggregateOptions{Filter: "@age > 15", DialectVersion: dlc, SortBy: []redis.FTAggregateSortBy{{FieldName: "@age"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19")) + Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25")) + } + + }) + + It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() { + client.HSet(ctx, "doc1", "foo", "bar") + + text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err := client.FTSearch(ctx, "idx1", "@foo:bar").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(int64(0))) + }) + + It("should FTCreate json", Label("search", "ftcreate"), func() { + + text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`) + + res, err := client.FTSearch(ctx, "idx1", "henry").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry"}`)) + }) + + It("should FTCreate json fields as names", Label("search", "ftcreate"), func() { + + text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText, As: "name"} + num1 := &redis.FieldSchema{FieldName: "$.age", FieldType: redis.SearchFieldTypeNumeric, As: "just_a_number"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.JSONSet(ctx, "doc:1", "$", `{"name": "Jon", "age": 25}`) + + res, err := client.FTSearchWithArgs(ctx, "idx1", "Jon", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}, {FieldName: "just_a_number"}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("Jon")) + Expect(res.Docs[0].Fields["just_a_number"]).To(BeEquivalentTo("25")) + }) + + It("should FTCreate CaseSensitive", Label("search", "ftcreate"), func() { + + tag1 := &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, CaseSensitive: false} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "1", "t", "HELLO") + client.HSet(ctx, "2", "t", "hello") + + res, err := client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("2")) + + resDrop, err := client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDrop).To(BeEquivalentTo("OK")) + + tag2 := &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, CaseSensitive: true} + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, tag2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err = client.FTSearch(ctx, "idx1", "@t:{HELLO}").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("1")) + + }) + + It("should FTSearch ReturnFields", Label("search", "ftsearch"), func() { + resJson, err := client.JSONSet(ctx, "doc:1", "$", `{"t": "riceratops","t2": "telmatosaurus", "n": 9072, "flt": 97.2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resJson).To(BeEquivalentTo("OK")) + + text1 := &redis.FieldSchema{FieldName: "$.t", FieldType: redis.SearchFieldTypeText} + num1 := &redis.FieldSchema{FieldName: "$.flt", FieldType: redis.SearchFieldTypeNumeric} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err := client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t", As: "txt"}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("riceratops")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "*", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.t2", As: "txt"}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["txt"]).To(BeEquivalentTo("telmatosaurus")) + }) + + It("should FTSynUpdate", Label("search", "ftsynupdate"), func() { + + text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resSynUpdate, err := client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"boy", "child", "offspring"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc1", "title", "he is a baby", "body", "this is a test") + + resSynUpdate, err = client.FTSynUpdateWithArgs(ctx, "idx1", "id1", &redis.FTSynUpdateOptions{SkipInitialScan: true}, []interface{}{"baby"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + client.HSet(ctx, "doc2", "title", "he is another baby", "body", "another test") + + res, err := client.FTSearchWithArgs(ctx, "idx1", "child", &redis.FTSearchOptions{Expander: "SYNONYM"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2")) + Expect(res.Docs[0].Fields["title"]).To(BeEquivalentTo("he is another baby")) + Expect(res.Docs[0].Fields["body"]).To(BeEquivalentTo("another test")) + }) + + It("should FTSynDump", Label("search", "ftsyndump"), func() { + + text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynDump, err := client.FTSynDump(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynDump[0].Term).To(BeEquivalentTo("baby")) + Expect(resSynDump[0].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[1].Term).To(BeEquivalentTo("wood")) + Expect(resSynDump[1].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[2].Term).To(BeEquivalentTo("boy")) + Expect(resSynDump[2].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[3].Term).To(BeEquivalentTo("tree")) + Expect(resSynDump[3].Synonyms).To(BeEquivalentTo([]string{"id1"})) + Expect(resSynDump[4].Term).To(BeEquivalentTo("child")) + Expect(resSynDump[4].Synonyms).To(Or(BeEquivalentTo([]string{"id1"}), BeEquivalentTo([]string{"id1", "id1"}))) + Expect(resSynDump[5].Term).To(BeEquivalentTo("offspring")) + Expect(resSynDump[5].Synonyms).To(BeEquivalentTo([]string{"id1"})) + + }) + + It("should FTCreate json with alias", Label("search", "ftcreate"), func() { + + text1 := &redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText, As: "name"} + num1 := &redis.FieldSchema{FieldName: "$.num", FieldType: redis.SearchFieldTypeNumeric, As: "num"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "num": 42}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james", "num": 3.14}`) + + res, err := client.FTSearch(ctx, "idx1", "@name:henry").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","num":42}`)) + + res, err = client.FTSearch(ctx, "idx1", "@num:[0 10]").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:2")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"james","num":3.14}`)) + }) + + It("should FTCreate json with multipath", Label("search", "ftcreate"), func() { + + tag1 := &redis.FieldSchema{FieldName: "$..name", FieldType: redis.SearchFieldTypeTag, As: "name"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true, Prefix: []interface{}{"king:"}}, tag1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry", "country": {"name": "england"}}`) + + res, err := client.FTSearch(ctx, "idx1", "@name:{england}").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("king:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"name":"henry","country":{"name":"england"}}`)) + }) + + It("should FTCreate json with jsonpath", Label("search", "ftcreate"), func() { + + text1 := &redis.FieldSchema{FieldName: `$["prod:name"]`, FieldType: redis.SearchFieldTypeText, As: "name"} + text2 := &redis.FieldSchema{FieldName: `$.prod:name`, FieldType: redis.SearchFieldTypeText, As: "name_unsupported"} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnJSON: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.JSONSet(ctx, "doc:1", "$", `{"prod:name": "RediSearch"}`) + + res, err := client.FTSearch(ctx, "idx1", "@name:RediSearch").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["$"]).To(BeEquivalentTo(`{"prod:name":"RediSearch"}`)) + + res, err = client.FTSearch(ctx, "idx1", "@name_unsupported:RediSearch").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "@name:RediSearch", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "name"}}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(1)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc:1")) + Expect(res.Docs[0].Fields["name"]).To(BeEquivalentTo("RediSearch")) + + }) + + It("should FTCreate VECTOR", Label("search", "ftcreate"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + + It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "name", "Alice") + client.HSet(ctx, "doc2", "name", "Bob") + client.HSet(ctx, "doc3", "name", "Carol") + + res1, err := client.FTSearchWithArgs(ctx, "idx1", "@name:($name1 | $name2 )", &redis.FTSearchOptions{Params: map[string]interface{}{"name1": "Alice", "name2": "Bob"}, DialectVersion: 2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(2))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + + }) + + It("should FTCreate and FTSearch numeric params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "numval", FieldType: redis.SearchFieldTypeNumeric}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "numval", 101) + client.HSet(ctx, "doc2", "numval", 102) + client.HSet(ctx, "doc3", "numval", 103) + + res1, err := client.FTSearchWithArgs(ctx, "idx1", "@numval:[$min $max]", &redis.FTSearchOptions{Params: map[string]interface{}{"min": 101, "max": 102}, DialectVersion: 2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(2))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + + }) + + It("should FTCreate and FTSearch geo params", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "g", FieldType: redis.SearchFieldTypeGeo}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "g", "29.69465, 34.95126") + client.HSet(ctx, "doc2", "g", "29.69350, 34.94737") + client.HSet(ctx, "doc3", "g", "29.68746, 34.94882") + + res1, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[$lon $lat $radius $units]", &redis.FTSearchOptions{Params: map[string]interface{}{"lat": "34.95126", "lon": "29.69465", "radius": 1000, "units": "km"}, DialectVersion: 2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(3))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("doc1")) + Expect(res1.Docs[1].ID).To(BeEquivalentTo("doc2")) + Expect(res1.Docs[2].ID).To(BeEquivalentTo("doc3")) + + }) + + It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + }) + + It("should FTCreate WithSuffixtrie", Label("search", "ftcreate", "ftinfo"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err := client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Attributes[0].Attribute).To(BeEquivalentTo("txt")) + + resDrop, err := client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDrop).To(BeEquivalentTo("OK")) + + // create withsuffixtrie index - text field + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, WithSuffixtrie: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err = client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue()) + + resDrop, err = client.FTDropIndex(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDrop).To(BeEquivalentTo("OK")) + + // create withsuffixtrie index - tag field + val, err = client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeTag, WithSuffixtrie: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + res, err = client.FTInfo(ctx, "idx1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue()) + }) + + It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "geom", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: "FLAT"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "small", "geom", "POLYGON((1 1, 1 100, 100 100, 100 1, 1 1))") + client.HSet(ctx, "large", "geom", "POLYGON((1 1, 1 200, 200 200, 200 1, 1 1))") + + res1, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[WITHIN $poly]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"poly": "POLYGON((0 0, 0 150, 150 150, 150 0, 0 0))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(BeEquivalentTo(int64(1))) + Expect(res1.Docs[0].ID).To(BeEquivalentTo("small")) + + res2, err := client.FTSearchWithArgs(ctx, "idx1", "@geom:[CONTAINS $poly]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"poly": "POLYGON((2 2, 2 50, 50 50, 50 2, 2 2))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(BeEquivalentTo(int64(2))) + }) +}) + +// It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() { +// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "1", "t", "hello") +// client.HSet(ctx, "2", "t", "world") + +// // FTProfile Search +// query := redis.FTSearchQuery("hello|world", &redis.FTSearchOptions{NoContent: true}) +// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// panic(res1) +// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// Expect(resProfile["Parsing time"].(float64) < 0.5).To(BeTrue()) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2.0)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("UNION")) + +// // FTProfile Aggregate +// aggQuery := redis.FTAggregateQuery("*", &redis.FTAggregateOptions{ +// Load: []redis.FTAggregateLoad{{Field: "t"}}, +// Apply: []redis.FTAggregateApply{{Field: "startswith(@t, 'hel')", As: "prefix"}}}) +// res2, err := client.FTProfile(ctx, "idx1", false, aggQuery).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(len(res2["results"].([]interface{}))).To(BeEquivalentTo(2)) +// resProfile = res2["profile"].(map[interface{}]interface{}) +// iterProfile0 = resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("WILDCARD")) +// }) + +// It("should FTProfile Search Limited", Label("search", "ftprofile"), func() { +// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "1", "t", "hello") +// client.HSet(ctx, "2", "t", "hell") +// client.HSet(ctx, "3", "t", "help") +// client.HSet(ctx, "4", "t", "helowa") + +// // FTProfile Search +// query := redis.FTSearchQuery("%hell% hel*", &redis.FTSearchOptions{}) +// res1, err := client.FTProfile(ctx, "idx1", true, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo("INTERSECT")) +// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) +// Expect(iterProfile0["Child iterators"].([]interface{})[0].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 3")) +// Expect(iterProfile0["Child iterators"].([]interface{})[1].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 4")) +// }) + +// It("should FTProfile Search query params", Label("search", "ftprofile"), func() { +// hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} +// val, err := client.FTCreate(ctx, "idx1", +// &redis.FTCreateOptions{}, +// &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() +// Expect(err).NotTo(HaveOccurred()) +// Expect(val).To(BeEquivalentTo("OK")) +// WaitForIndexing(client, "idx1") + +// client.HSet(ctx, "a", "v", "aaaaaaaa") +// client.HSet(ctx, "b", "v", "aaaabaaa") +// client.HSet(ctx, "c", "v", "aaaaabaa") + +// // FTProfile Search +// searchOptions := &redis.FTSearchOptions{ +// Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, +// SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, +// DialectVersion: 2, +// Params: map[string]interface{}{"vec": "aaaaaaaa"}, +// } +// query := redis.FTSearchQuery("*=>[KNN 2 @v $vec]", searchOptions) +// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() +// Expect(err).NotTo(HaveOccurred()) +// resProfile := res1["profile"].(map[interface{}]interface{}) +// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) +// Expect(iterProfile0["Type"]).To(BeEquivalentTo(redis.SearchFieldTypeVector.String())) +// Expect(res1["total_results"]).To(BeEquivalentTo(2)) +// results0 := res1["results"].([]interface{})[0].(map[interface{}]interface{}) +// Expect(results0["id"]).To(BeEquivalentTo("a")) +// Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) +// }) From 41a06555e0122c4f56c6bc371e1425a3b74074a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:06:39 +0800 Subject: [PATCH 018/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3028) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.36.0 to 0.38.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.36.0...0.38.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Monkey --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index f739a54242..62e38997e4 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.36.0 + uses: rojopolis/spellcheck-github-actions@0.38.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 26f69c6304937dfbc5d94b6b6c2053f2f02fd6e3 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:10:30 +0300 Subject: [PATCH 019/230] Change monitor test to run manually (#3041) * Change monitor test to run manually * fix --- monitor_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/monitor_test.go b/monitor_test.go index 91a7334078..96c33bf1ec 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -2,10 +2,10 @@ package redis_test import ( "context" + "os" "strings" - "time" - "testing" + "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" @@ -13,13 +13,18 @@ import ( "github.com/redis/go-redis/v9" ) +// This test is for manual use and is not part of the CI of Go-Redis. var _ = Describe("Monitor command", Label("monitor"), func() { ctx := context.TODO() var client *redis.Client BeforeEach(func() { + if os.Getenv("RUN_MONITOR_TEST") != "true" { + Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") + } client = redis.NewClient(&redis.Options{Addr: ":6379"}) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) AfterEach(func() { @@ -51,6 +56,10 @@ var _ = Describe("Monitor command", Label("monitor"), func() { }) func TestMonitorCommand(t *testing.T) { + if os.Getenv("RUN_MONITOR_TEST") != "true" { + t.Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") + } + ctx := context.TODO() client := redis.NewClient(&redis.Options{Addr: ":6379"}) if err := client.FlushDB(ctx).Err(); err != nil { From 8bfdb826c7d5805fe9c8406bf90b68b19a8130b6 Mon Sep 17 00:00:00 2001 From: Immersed <167606329+immersedin@users.noreply.github.com> Date: Fri, 5 Jul 2024 05:27:44 +0300 Subject: [PATCH 020/230] Update pubsub.go (#3042) Change context.Background() to ctx --- pubsub.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubsub.go b/pubsub.go index aea96241f3..72b18f49a7 100644 --- a/pubsub.go +++ b/pubsub.go @@ -84,7 +84,7 @@ func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, er } func (c *PubSub) writeCmd(ctx context.Context, cn *pool.Conn, cmd Cmder) error { - return cn.WithWriter(context.Background(), c.opt.WriteTimeout, func(wr *proto.Writer) error { + return cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmd(wr, cmd) }) } From f5a789a84676cfe30e0d0b1eac01cdc3307d8c97 Mon Sep 17 00:00:00 2001 From: Srikar Jilugu <139106260+srikar-jilugu@users.noreply.github.com> Date: Wed, 10 Jul 2024 08:56:27 +0530 Subject: [PATCH 021/230] fix node routing in slotClosestNode (#3043) * fix node routing when all nodes are failing * fix minlatency zero value --- osscluster.go | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/osscluster.go b/osscluster.go index e28cb1e391..73a9e2b743 100644 --- a/osscluster.go +++ b/osscluster.go @@ -341,6 +341,8 @@ func (n *clusterNode) Close() error { return n.Client.Close() } +const maximumNodeLatency = 1 * time.Minute + func (n *clusterNode) updateLatency() { const numProbe = 10 var dur uint64 @@ -361,7 +363,7 @@ func (n *clusterNode) updateLatency() { if successes == 0 { // If none of the pings worked, set latency to some arbitrarily high value so this node gets // least priority. - latency = float64((1 * time.Minute) / time.Microsecond) + latency = float64((maximumNodeLatency) / time.Microsecond) } else { latency = float64(dur) / float64(successes) } @@ -735,20 +737,40 @@ func (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) { return c.nodes.Random() } - var node *clusterNode + var allNodesFailing = true + var ( + closestNonFailingNode *clusterNode + closestNode *clusterNode + minLatency time.Duration + ) + + // setting the max possible duration as zerovalue for minlatency + minLatency = time.Duration(math.MaxInt64) + for _, n := range nodes { - if n.Failing() { - continue - } - if node == nil || n.Latency() < node.Latency() { - node = n + if closestNode == nil || n.Latency() < minLatency { + closestNode = n + minLatency = n.Latency() + if !n.Failing() { + closestNonFailingNode = n + allNodesFailing = false + } } } - if node != nil { - return node, nil + + // pick the healthly node with the lowest latency + if !allNodesFailing && closestNonFailingNode != nil { + return closestNonFailingNode, nil + } + + // if all nodes are failing, we will pick the temporarily failing node with lowest latency + if minLatency < maximumNodeLatency && closestNode != nil { + internal.Logger.Printf(context.TODO(), "redis: all nodes are marked as failed, picking the temporarily failing node with lowest latency") + return closestNode, nil } - // If all nodes are failing - return random node + // If all nodes are having the maximum latency(all pings are failing) - return a random node across the cluster + internal.Logger.Printf(context.TODO(), "redis: pings to all nodes are failing, picking a random node across the cluster") return c.nodes.Random() } From 711fe9f5c66d12c89f8b4362b2a3c4d68986b2b0 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:30:32 +0300 Subject: [PATCH 022/230] Support Hash-field expiration for 7.4 CE RC2 (#3040) Co-authored-by: Monkey Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- Makefile | 2 +- commands_test.go | 45 ++++++++++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 00cf1de584..d8d007596b 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ build: testdata/redis: mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.4-rc1.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- https://download.redis.io/releases/redis-7.4-rc2.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis cd $< && make all diff --git a/commands_test.go b/commands_test.go index edc9569435..cca4239446 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2486,21 +2486,25 @@ var _ = Describe("Commands", func() { }) It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { - res, err := client.HExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + resEmpty, err := client.HExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err = client.HExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, 1, -2})) }) It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { - _, err := client.HPExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + resEmpty, err := client.HPExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2512,9 +2516,10 @@ var _ = Describe("Commands", func() { }) It("should HExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + resEmpty, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2526,9 +2531,10 @@ var _ = Describe("Commands", func() { }) It("should HPExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + resEmpty, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2540,9 +2546,10 @@ var _ = Describe("Commands", func() { }) It("should HPersist", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result() + resEmpty, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2562,9 +2569,10 @@ var _ = Describe("Commands", func() { }) It("should HExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + resEmpty, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2580,9 +2588,10 @@ var _ = Describe("Commands", func() { }) It("should HPExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + resEmpty, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2599,9 +2608,10 @@ var _ = Describe("Commands", func() { }) It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + resEmpty, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2617,9 +2627,10 @@ var _ = Describe("Commands", func() { }) It("should HPTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { - - _, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + resEmpty, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) + Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) From 8f4d4f700c3e0efc466119a62863e39c0e6ee618 Mon Sep 17 00:00:00 2001 From: tzongw Date: Thu, 11 Jul 2024 13:08:23 +0800 Subject: [PATCH 023/230] Support Hash-field expiration commands in Pipeline & Fix HExpire HExpireWithArgs expiration (#3038) * Support Hash-field expiration commands in Pipeline * Fix HExpire & HExpireWithArgs expiration * Fix HExpire & HPExpire Testcase * Update commands_test.go --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Monkey --- commands_test.go | 20 ++++++++++---------- hash_commands.go | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/commands_test.go b/commands_test.go index cca4239446..9554bf9a9f 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2486,31 +2486,31 @@ var _ = Describe("Commands", func() { }) It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { - resEmpty, err := client.HExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + res, err := client.HExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) - Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2})) for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + res, err = client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key2", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, 1, -2})) }) It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { - resEmpty, err := client.HPExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) - Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) + Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2})) for i := 0; i < 100; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HPExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + res, err = client.HPExpire(ctx, "myhash", 10*time.Second, "key1", "key2", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, 1, -2})) }) @@ -2559,7 +2559,7 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{-1, -1, -2})) - res, err = client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + res, err = client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, -2})) @@ -2578,7 +2578,7 @@ var _ = Describe("Commands", func() { Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, -2})) @@ -2617,7 +2617,7 @@ var _ = Describe("Commands", func() { Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, -2})) @@ -2636,7 +2636,7 @@ var _ = Describe("Commands", func() { Expect(sadd.Err()).NotTo(HaveOccurred()) } - res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + res, err := client.HExpire(ctx, "myhash", 10*time.Second, "key1", "key200").Result() Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal([]int64{1, -2})) diff --git a/hash_commands.go b/hash_commands.go index ef69064e0d..dcffdcdd98 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -23,6 +23,19 @@ type HashCmdable interface { HVals(ctx context.Context, key string) *StringSliceCmd HRandField(ctx context.Context, key string, count int) *StringSliceCmd HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd + HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd + HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd + HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd + HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd + HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd + HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd + HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd + HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd + HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd + HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd + HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd + HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd + HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd } func (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd { @@ -202,7 +215,7 @@ type HExpireArgs struct { // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. // For more information - https://redis.io/commands/hexpire/ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { - args := []interface{}{"HEXPIRE", key, expiration, "FIELDS", len(fields)} + args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration), "FIELDS", len(fields)} for _, field := range fields { args = append(args, field) @@ -217,7 +230,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. // For more information - https://redis.io/commands/hexpire/ func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { - args := []interface{}{"HEXPIRE", key, expiration} + args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration)} // only if one argument is true, we can add it to the args // if more than one argument is true, it will cause an error From 71ac8c8e5b5ec2104ca974c2b795d21e60df23fc Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:57:56 +0300 Subject: [PATCH 024/230] TimeSeries insertion filters for close samples (#3003) * TimeSeries insertion filters for close samples * fix * fix * fix * fix * fix --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- timeseries_commands.go | 56 ++++++++++---- timeseries_commands_test.go | 149 ++++++++++++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 19 deletions(-) diff --git a/timeseries_commands.go b/timeseries_commands.go index 6f1b2fa458..82d8cdfcf5 100644 --- a/timeseries_commands.go +++ b/timeseries_commands.go @@ -40,25 +40,32 @@ type TimeseriesCmdable interface { } type TSOptions struct { - Retention int - ChunkSize int - Encoding string - DuplicatePolicy string - Labels map[string]string + Retention int + ChunkSize int + Encoding string + DuplicatePolicy string + Labels map[string]string + IgnoreMaxTimeDiff int64 + IgnoreMaxValDiff float64 } type TSIncrDecrOptions struct { - Timestamp int64 - Retention int - ChunkSize int - Uncompressed bool - Labels map[string]string + Timestamp int64 + Retention int + ChunkSize int + Uncompressed bool + DuplicatePolicy string + Labels map[string]string + IgnoreMaxTimeDiff int64 + IgnoreMaxValDiff float64 } type TSAlterOptions struct { - Retention int - ChunkSize int - DuplicatePolicy string - Labels map[string]string + Retention int + ChunkSize int + DuplicatePolicy string + Labels map[string]string + IgnoreMaxTimeDiff int64 + IgnoreMaxValDiff float64 } type TSCreateRuleOptions struct { @@ -223,6 +230,9 @@ func (c cmdable) TSAddWithArgs(ctx context.Context, key string, timestamp interf args = append(args, label, value) } } + if options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 { + args = append(args, "IGNORE", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff) + } } cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) @@ -264,6 +274,9 @@ func (c cmdable) TSCreateWithArgs(ctx context.Context, key string, options *TSOp args = append(args, label, value) } } + if options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 { + args = append(args, "IGNORE", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff) + } } cmd := NewStatusCmd(ctx, args...) _ = c(ctx, cmd) @@ -292,6 +305,9 @@ func (c cmdable) TSAlter(ctx context.Context, key string, options *TSAlterOption args = append(args, label, value) } } + if options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 { + args = append(args, "IGNORE", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff) + } } cmd := NewStatusCmd(ctx, args...) _ = c(ctx, cmd) @@ -351,12 +367,18 @@ func (c cmdable) TSIncrByWithArgs(ctx context.Context, key string, timestamp flo if options.Uncompressed { args = append(args, "UNCOMPRESSED") } + if options.DuplicatePolicy != "" { + args = append(args, "DUPLICATE_POLICY", options.DuplicatePolicy) + } if options.Labels != nil { args = append(args, "LABELS") for label, value := range options.Labels { args = append(args, label, value) } } + if options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 { + args = append(args, "IGNORE", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff) + } } cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) @@ -391,12 +413,18 @@ func (c cmdable) TSDecrByWithArgs(ctx context.Context, key string, timestamp flo if options.Uncompressed { args = append(args, "UNCOMPRESSED") } + if options.DuplicatePolicy != "" { + args = append(args, "DUPLICATE_POLICY", options.DuplicatePolicy) + } if options.Labels != nil { args = append(args, "LABELS") for label, value := range options.Labels { args = append(args, label, value) } } + if options.IgnoreMaxTimeDiff != 0 || options.IgnoreMaxValDiff != 0 { + args = append(args, "IGNORE", options.IgnoreMaxTimeDiff, options.IgnoreMaxValDiff) + } } cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) diff --git a/timeseries_commands_test.go b/timeseries_commands_test.go index 563f24e79b..c62367a768 100644 --- a/timeseries_commands_test.go +++ b/timeseries_commands_test.go @@ -23,7 +23,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { Expect(client.Close()).NotTo(HaveOccurred()) }) - It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs"), func() { + It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { result, err := client.TSCreate(ctx, "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo("OK")) @@ -62,10 +62,60 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { resultInfo, err = client.TSInfo(ctx, keyName).Result() Expect(err).NotTo(HaveOccurred()) Expect(strings.ToUpper(resultInfo["duplicatePolicy"].(string))).To(BeEquivalentTo(dup)) - } + // Test insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: "LAST", IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1020, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1020)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1021, 22.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1021)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1021).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(4)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1020, Value: 11.5}, + {Timestamp: 1021, Value: 22.0}})) + // Test insertion filters with other duplicate policy + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-2", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd1, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1000)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1010)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1013)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) }) - It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs"), func() { + It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { result, err := client.TSAdd(ctx, "1", 1, 1).Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo(1)) @@ -138,9 +188,23 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { resultGet, err = client.TSGet(ctx, "tsami-1").Result() Expect(err).NotTo(HaveOccurred()) Expect(resultGet.Value).To(BeEquivalentTo(5)) + // Insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1000, 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1004, 3.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) }) - It("should TSAlter", Label("timeseries", "tsalter"), func() { + It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { result, err := client.TSCreate(ctx, "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo("OK")) @@ -179,6 +243,33 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { resultInfo, err = client.TSInfo(ctx, "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("min")) + // Test insertion filters + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + alterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + resultAlter, err = client.TSAlter(ctx, "ts-if-1", alterOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1015, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) }) It("should TSCreateRule and TSDeleteRule", Label("timeseries", "tscreaterule", "tsdeleterule"), func() { @@ -216,7 +307,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { Expect(resultInfo["rules"]).To(BeEquivalentTo(map[interface{}]interface{}{})) }) - It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs"), func() { + It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { for i := 0; i < 100; i++ { _, err := client.TSIncrBy(ctx, "1", 1).Result() Expect(err).NotTo(HaveOccurred()) @@ -277,6 +368,54 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { resultInfo, err = client.TSInfo(ctx, "4").Result() Expect(err).NotTo(HaveOccurred()) Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + + // Test insertion filters INCRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err := client.TSIncrByWithArgs(ctx, "ts-if-1", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}})) + + // Test insertion filters DECRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}})) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}})) }) It("should TSGet", Label("timeseries", "tsget"), func() { From 516652021d49d3a318338a2f49b7ea9af2479eef Mon Sep 17 00:00:00 2001 From: naiqianz Date: Fri, 12 Jul 2024 11:16:21 +0800 Subject: [PATCH 025/230] add test for tls connCheck #3025 (#3047) * add a check for TLS connections. --- internal/pool/conn_check.go | 5 +++++ internal/pool/conn_check_test.go | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/internal/pool/conn_check.go b/internal/pool/conn_check.go index 83190d3948..07c261c2bb 100644 --- a/internal/pool/conn_check.go +++ b/internal/pool/conn_check.go @@ -3,6 +3,7 @@ package pool import ( + "crypto/tls" "errors" "io" "net" @@ -16,6 +17,10 @@ func connCheck(conn net.Conn) error { // Reset previous timeout. _ = conn.SetDeadline(time.Time{}) + // Check if tls.Conn. + if c, ok := conn.(*tls.Conn); ok { + conn = c.NetConn() + } sysConn, ok := conn.(syscall.Conn) if !ok { return nil diff --git a/internal/pool/conn_check_test.go b/internal/pool/conn_check_test.go index 2ade8a0b97..2149933390 100644 --- a/internal/pool/conn_check_test.go +++ b/internal/pool/conn_check_test.go @@ -3,6 +3,7 @@ package pool import ( + "crypto/tls" "net" "net/http/httptest" "time" @@ -14,12 +15,17 @@ import ( var _ = Describe("tests conn_check with real conns", func() { var ts *httptest.Server var conn net.Conn + var tlsConn *tls.Conn var err error BeforeEach(func() { ts = httptest.NewServer(nil) conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second) Expect(err).NotTo(HaveOccurred()) + tlsTestServer := httptest.NewUnstartedServer(nil) + tlsTestServer.StartTLS() + tlsConn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, tlsTestServer.Listener.Addr().Network(), tlsTestServer.Listener.Addr().String(), &tls.Config{InsecureSkipVerify: true}) + Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { @@ -33,11 +39,23 @@ var _ = Describe("tests conn_check with real conns", func() { Expect(connCheck(conn)).To(HaveOccurred()) }) + It("good tls conn check", func() { + Expect(connCheck(tlsConn)).NotTo(HaveOccurred()) + + Expect(tlsConn.Close()).NotTo(HaveOccurred()) + Expect(connCheck(tlsConn)).To(HaveOccurred()) + }) + It("bad conn check", func() { Expect(conn.Close()).NotTo(HaveOccurred()) Expect(connCheck(conn)).To(HaveOccurred()) }) + It("bad tls conn check", func() { + Expect(tlsConn.Close()).NotTo(HaveOccurred()) + Expect(connCheck(tlsConn)).To(HaveOccurred()) + }) + It("check conn deadline", func() { Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred()) time.Sleep(time.Millisecond * 10) From eb4d5f0bd926e66305bb4a8946ebbcb50a90dd3d Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Fri, 12 Jul 2024 23:55:12 -0700 Subject: [PATCH 026/230] Avoid unnecessary retry delay following MOVED and ASK redirection (#3048) --- osscluster.go | 6 ++++-- osscluster_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osscluster.go b/osscluster.go index 73a9e2b743..ce258ff363 100644 --- a/osscluster.go +++ b/osscluster.go @@ -938,10 +938,13 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { slot := c.cmdSlot(ctx, cmd) var node *clusterNode + var moved bool var ask bool var lastErr error for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { - if attempt > 0 { + // MOVED and ASK responses are not transient errors that require retry delay; they + // should be attempted immediately. + if attempt > 0 && !moved && !ask { if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { return err } @@ -985,7 +988,6 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { continue } - var moved bool var addr string moved, ask, addr = isMovedError(lastErr) if moved || ask { diff --git a/osscluster_test.go b/osscluster_test.go index 3d2f80711b..f7bd1683f8 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -653,6 +653,32 @@ var _ = Describe("ClusterClient", func() { Expect(client.Close()).NotTo(HaveOccurred()) }) + It("follows node redirection immediately", func() { + // Configure retry backoffs far in excess of the expected duration of redirection + opt := redisClusterOptions() + opt.MinRetryBackoff = 10 * time.Minute + opt.MaxRetryBackoff = 20 * time.Minute + client := cluster.newClusterClient(ctx, opt) + + Eventually(func() error { + return client.SwapNodes(ctx, "A") + }, 30*time.Second).ShouldNot(HaveOccurred()) + + // Note that this context sets a deadline more aggressive than the lowest possible bound + // of the retry backoff; this verifies that redirection completes immediately. + redirCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + err := client.Set(redirCtx, "A", "VALUE", 0).Err() + Expect(err).NotTo(HaveOccurred()) + + v, err := client.Get(redirCtx, "A").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(v).To(Equal("VALUE")) + + Expect(client.Close()).NotTo(HaveOccurred()) + }) + It("calls fn for every master node", func() { for i := 0; i < 10; i++ { Expect(client.Set(ctx, strconv.Itoa(i), "", 0).Err()).NotTo(HaveOccurred()) From d81c1998ceb96cab8c16cc7ed70cea943bbb6cf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 01:30:07 +0800 Subject: [PATCH 027/230] chore(deps): bump golangci/golangci-lint-action from 4 to 6 (#2993) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 6. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v6) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Monkey --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index a139f5daba..5210ccfa23 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -23,4 +23,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 From 8408e97cd815adedca83c776ab5b812d2383b186 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:48:41 +0300 Subject: [PATCH 028/230] Support RediSearch empty values (#3053) * Support RediSearch empty values * Remove from enterprise --- search_commands.go | 9 +++++ search_test.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/search_commands.go b/search_commands.go index 8214a570be..f5118c77e2 100644 --- a/search_commands.go +++ b/search_commands.go @@ -75,6 +75,8 @@ type FieldSchema struct { WithSuffixtrie bool VectorArgs *FTVectorArgs GeoShapeFieldType string + IndexEmpty bool + IndexMissing bool } type FTVectorArgs struct { @@ -1002,6 +1004,13 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.WithSuffixtrie { args = append(args, "WITHSUFFIXTRIE") } + if schema.IndexEmpty { + args = append(args, "INDEXEMPTY") + } + if schema.IndexMissing { + args = append(args, "INDEXMISSING") + + } } cmd := NewStatusCmd(ctx, args...) _ = c(ctx, cmd) diff --git a/search_test.go b/search_test.go index 60888ef5c2..d80aae0461 100644 --- a/search_test.go +++ b/search_test.go @@ -1043,6 +1043,96 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res2.Total).To(BeEquivalentTo(int64(2))) }) + + It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}}, + &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true}, + &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexMissing: true}, + &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexMissing: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "property:1", map[string]interface{}{ + "title": "Luxury Villa in Malibu", + "features": "pool,sea view,modern", + "description": "A stunning modern villa overlooking the Pacific Ocean.", + }) + + client.HSet(ctx, "property:2", map[string]interface{}{ + "title": "Downtown Flat", + "description": "Modern flat in central Paris with easy access to metro.", + }) + + client.HSet(ctx, "property:3", map[string]interface{}{ + "title": "Beachfront Bungalow", + "features": "beachfront,sun deck", + }) + + res, err := client.FTSearchWithArgs(ctx, "idx1", "ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@features)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "-ismissing(@description)", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2")) + }) + + It("should search empty fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}}, + &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true}, + &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true}, + &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, IndexEmpty: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "property:1", map[string]interface{}{ + "title": "Luxury Villa in Malibu", + "features": "pool,sea view,modern", + "description": "A stunning modern villa overlooking the Pacific Ocean.", + }) + + client.HSet(ctx, "property:2", map[string]interface{}{ + "title": "Downtown Flat", + "features": "", + "description": "Modern flat in central Paris with easy access to metro.", + }) + + client.HSet(ctx, "property:3", map[string]interface{}{ + "title": "Beachfront Bungalow", + "features": "beachfront,sun deck", + "description": "", + }) + + res, err := client.FTSearchWithArgs(ctx, "idx1", "@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:2")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "-@features:{\"\"}", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("property:3")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:3")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "-@description:''", &redis.FTSearchOptions{DialectVersion: 4, Return: []redis.FTSearchReturn{{FieldName: "id"}}, NoContent: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) + Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2")) + }) }) // It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() { From be3e4f011758d439780f48aa495d03fed628a805 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:41:48 +0300 Subject: [PATCH 029/230] Add tests for search GEO (#3051) * Add tests for search GEO * Remove from enterprise --- search_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/search_test.go b/search_test.go index d80aae0461..7edae6728b 100644 --- a/search_test.go +++ b/search_test.go @@ -1044,6 +1044,74 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(res2.Total).To(BeEquivalentTo(int64(2))) }) + It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() { + _, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{ + FieldName: "g", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }).Result() + Expect(err).NotTo(HaveOccurred()) + + client.HSet(ctx, "doc_point1", "g", "POINT (10 10)") + client.HSet(ctx, "doc_point2", "g", "POINT (50 50)") + client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))") + client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))") + + intersection, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[intersects $shape]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + _assert_geosearch_result(&intersection, []string{"doc_point2", "doc_polygon1"}) + + disjunction, err := client.FTSearchWithArgs(ctx, "idx1", "@g:[disjoint $shape]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + _assert_geosearch_result(&disjunction, []string{"doc_point1", "doc_polygon2"}) + }) + + It("should test geoshapes query contains and within", func() { + _, err := client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{}, &redis.FieldSchema{ + FieldName: "g", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }).Result() + Expect(err).NotTo(HaveOccurred()) + + client.HSet(ctx, "doc_point1", "g", "POINT (10 10)") + client.HSet(ctx, "doc_point2", "g", "POINT (50 50)") + client.HSet(ctx, "doc_polygon1", "g", "POLYGON ((20 20, 25 35, 35 25, 20 20))") + client.HSet(ctx, "doc_polygon2", "g", "POLYGON ((60 60, 65 75, 70 70, 65 55, 60 60))") + + containsA, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"shape": "POINT(25 25)"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + _assert_geosearch_result(&containsA, []string{"doc_polygon1"}) + + containsB, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[contains $shape]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"shape": "POLYGON((24 24, 24 26, 25 25, 24 24))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + _assert_geosearch_result(&containsB, []string{"doc_polygon1"}) + + within, err := client.FTSearchWithArgs(ctx, "idx2", "@g:[within $shape]", + &redis.FTSearchOptions{ + DialectVersion: 3, + Params: map[string]interface{}{"shape": "POLYGON((15 15, 75 15, 50 70, 20 40, 15 15))"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + _assert_geosearch_result(&within, []string{"doc_point2", "doc_polygon1"}) + }) + It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true}, @@ -1135,6 +1203,15 @@ var _ = Describe("RediSearch commands", Label("search"), func() { }) }) +func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { + ids := make([]string, len(result.Docs)) + for i, doc := range result.Docs { + ids[i] = doc.ID + } + Expect(ids).To(ConsistOf(expectedDocIDs)) + Expect(result.Total).To(BeEquivalentTo(len(expectedDocIDs))) +} + // It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() { // val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() // Expect(err).NotTo(HaveOccurred()) From bc524fe0345cc92a82fc947a5c5bd102467e54f2 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:55:58 +0300 Subject: [PATCH 030/230] Test RediSearch dialect 4 (#3052) * Test dialect 4 * Add support for num and email * remove tests from RE --- search_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/search_test.go b/search_test.go index 7edae6728b..0d66f243f7 100644 --- a/search_test.go +++ b/search_test.go @@ -1017,6 +1017,110 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(res.Attributes[0].WithSuffixtrie).To(BeTrue()) }) + It("should test dialect 4", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{ + Prefix: []interface{}{"resource:"}, + }, &redis.FieldSchema{ + FieldName: "uuid", + FieldType: redis.SearchFieldTypeTag, + }, &redis.FieldSchema{ + FieldName: "tags", + FieldType: redis.SearchFieldTypeTag, + }, &redis.FieldSchema{ + FieldName: "description", + FieldType: redis.SearchFieldTypeText, + }, &redis.FieldSchema{ + FieldName: "rating", + FieldType: redis.SearchFieldTypeNumeric, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + client.HSet(ctx, "resource:1", map[string]interface{}{ + "uuid": "123e4567-e89b-12d3-a456-426614174000", + "tags": "finance|crypto|$btc|blockchain", + "description": "Analysis of blockchain technologies & Bitcoin's potential.", + "rating": 5, + }) + client.HSet(ctx, "resource:2", map[string]interface{}{ + "uuid": "987e6543-e21c-12d3-a456-426614174999", + "tags": "health|well-being|fitness|new-year's-resolutions", + "description": "Health trends for the new year, including fitness regimes.", + "rating": 4, + }) + + res, err := client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}", + &redis.FTSearchOptions{ + DialectVersion: 2, + Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(int64(1))) + Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1")) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "@uuid:{$uuid}", + &redis.FTSearchOptions{ + DialectVersion: 4, + Params: map[string]interface{}{"uuid": "123e4567-e89b-12d3-a456-426614174000"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(int64(1))) + Expect(res.Docs[0].ID).To(BeEquivalentTo("resource:1")) + + client.HSet(ctx, "test:1", map[string]interface{}{ + "uuid": "3d3586fe-0416-4572-8ce", + "email": "adriano@acme.com.ie", + "num": 5, + }) + + // Create the index + ftCreateOptions := &redis.FTCreateOptions{ + Prefix: []interface{}{"test:"}, + } + schema := []*redis.FieldSchema{ + { + FieldName: "uuid", + FieldType: redis.SearchFieldTypeTag, + }, + { + FieldName: "email", + FieldType: redis.SearchFieldTypeTag, + }, + { + FieldName: "num", + FieldType: redis.SearchFieldTypeNumeric, + }, + } + + val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("OK")) + + ftSearchOptions := &redis.FTSearchOptions{ + DialectVersion: 4, + Params: map[string]interface{}{ + "uuid": "3d3586fe-0416-4572-8ce", + "email": "adriano@acme.com.ie", + }, + } + + res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@uuid:{$uuid}", ftSearchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1")) + Expect(res.Docs[0].Fields["uuid"]).To(BeEquivalentTo("3d3586fe-0416-4572-8ce")) + + res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@email:{$email}", ftSearchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1")) + Expect(res.Docs[0].Fields["email"]).To(BeEquivalentTo("adriano@acme.com.ie")) + + ftSearchOptions.Params = map[string]interface{}{"num": 5} + res, err = client.FTSearchWithArgs(ctx, "idx_hash", "@num:[5]", ftSearchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("test:1")) + Expect(res.Docs[0].Fields["num"]).To(BeEquivalentTo("5")) + }) + It("should FTCreate GeoShape", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "geom", FieldType: redis.SearchFieldTypeGeoShape, GeoShapeFieldType: "FLAT"}).Result() Expect(err).NotTo(HaveOccurred()) From d5d21398f6c578faaa34cd2edf9566adb6d80227 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:56:58 +0300 Subject: [PATCH 031/230] Add tests case with FLOAT16 and BFLOAT16 vectors (#3054) * Add tests case with FLOAT16 and BFLOAT16 vectors * Remove from enterprise --- search_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/search_test.go b/search_test.go index 0d66f243f7..0e1a473b88 100644 --- a/search_test.go +++ b/search_test.go @@ -1148,6 +1148,16 @@ var _ = Describe("RediSearch commands", Label("search"), func() { Expect(res2.Total).To(BeEquivalentTo(int64(2))) }) + It("should create search index with FLOAT16 and BFLOAT16 vectors", Label("search", "ftcreate", "NonRedisEnterprise"), func() { + val, err := client.FTCreate(ctx, "index", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "float16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "FLOAT16", Dim: 768, DistanceMetric: "COSINE"}}}, + &redis.FieldSchema{FieldName: "bfloat16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "BFLOAT16", Dim: 768, DistanceMetric: "COSINE"}}}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "index") + }) + It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() { _, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{ FieldName: "g", From 91870a196e9361eea7a24f939a7e5ae24d2f99c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:13:40 +0300 Subject: [PATCH 032/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3067) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.38.0 to 0.40.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.38.0...0.40.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 62e38997e4..cc6d828c93 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.38.0 + uses: rojopolis/spellcheck-github-actions@0.40.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From db7a9e9f716d07ae7b70ccd21129ee2d2b08672d Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:14:06 +0300 Subject: [PATCH 033/230] Retract versions 9.5.3 and 9.5.4 (#3068) --- go.mod | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/go.mod b/go.mod index 6c65f094fa..bd13d74530 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,8 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ) + +retract ( + v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. + v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. +) From 4f7b394ac921f88319ef322d5acc0007e3c17b93 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:15:15 +0300 Subject: [PATCH 034/230] Updated module version that points to retracted package version (#3074) * Updated module version that points to retracted package version * Updated testing image to latest --- .github/workflows/build.yml | 2 +- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4061bbdff5..bef0628402 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: services: redis: - image: redis/redis-stack-server:edge + image: redis/redis-stack-server:latest options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 468d0a54fa..715454c657 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.6.1 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 0126764ef4..f68ff25d55 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.3 +require github.com/redis/go-redis/v9 v9.6.1 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 3f4c29d12b..176e03d095 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.3 +require github.com/redis/go-redis/v9 v9.6.1 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index fea4e72a51..2b5030ab63 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/extra/redisotel/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.1 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 09fb5bed4a..d8e9bfffe0 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.5.3 +require github.com/redis/go-redis/v9 v9.6.1 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index c01e312933..45423ec525 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.6.1 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index d623cef367..7b99a888e3 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.1 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index a035c86943..2884faf9a7 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.6.1 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 587d3bc3a4..12dd414a16 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.1 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index fcc35b9bd9..aedca634a4 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.6.1 ) require ( diff --git a/version.go b/version.go index 2ea7df99a7..b1234dac3a 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.5.3" + return "9.6.1" } From 4c0e6793dd43ea02e2edfdb1a662c099d2af4c58 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:09:47 +0300 Subject: [PATCH 035/230] Add test coverage reporting and Codecov badge (#3055) * Add codecov to ci * Add codecov to ci * update yanl * Add changes * Add changes * test * Add changes * Add changes * Add changes --- .github/workflows/build.yml | 6 ++++++ Makefile | 1 + README.md | 1 + 3 files changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bef0628402..5007423a4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,3 +37,9 @@ jobs: - name: Test run: make test + + - name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage.txt + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/Makefile b/Makefile index d8d007596b..1a6bd17862 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ test: testdeps go test ./... -short -race && \ go test ./... -run=NONE -bench=. -benchmem && \ env GOOS=linux GOARCH=386 go test && \ + go test -coverprofile=coverage.txt -covermode=atomic ./... && \ go vet); \ done cd internal/customvet && go build . diff --git a/README.md b/README.md index e7df5dfd60..c7951a4d4b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc) [![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/) +[![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) [![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj) > go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). From 2f4b81a2808af683f13713a356adf0e1e2a4b786 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:49:25 +0100 Subject: [PATCH 036/230] DOC-4213 string code examples (#3102) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/string_example_test.go | 173 ++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 doctests/string_example_test.go diff --git a/doctests/string_example_test.go b/doctests/string_example_test.go new file mode 100644 index 0000000000..20ca855489 --- /dev/null +++ b/doctests/string_example_test.go @@ -0,0 +1,173 @@ +// EXAMPLE: set_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END +func ExampleClient_set_get() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + // STEP_START set_get + res1, err := rdb.Set(ctx, "bike:1", "Deimos", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.Get(ctx, "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> Deimos + // STEP_END + + // Output: + // OK + // Deimos +} + +func ExampleClient_setnx_xx() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Set(ctx, "bike:1", "Deimos", 0) + // REMOVE_END + + // STEP_START setnx_xx + res3, err := rdb.SetNX(ctx, "bike:1", "bike", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> false + + res4, err := rdb.Get(ctx, "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> Deimos + + res5, err := rdb.SetXX(ctx, "bike:1", "bike", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> OK + // STEP_END + + // Output: + // false + // Deimos + // true +} + +func ExampleClient_mset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1", "bike:2", "bike:3") + // REMOVE_END + + // STEP_START mset + res6, err := rdb.MSet(ctx, "bike:1", "Deimos", "bike:2", "Ares", "bike:3", "Vanth").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> OK + + res7, err := rdb.MGet(ctx, "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> [Deimos Ares Vanth] + // STEP_END + + // Output: + // OK + // [Deimos Ares Vanth] +} + +func ExampleClient_incr() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "total_crashes") + // REMOVE_END + + // STEP_START incr + res8, err := rdb.Set(ctx, "total_crashes", "0", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> OK + + res9, err := rdb.Incr(ctx, "total_crashes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 1 + + res10, err := rdb.IncrBy(ctx, "total_crashes", 10).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 11 + // STEP_END + + // Output: + // OK + // 1 + // 11 +} From aafbf706ab2efa7008f39d4b316928be7aacdd8f Mon Sep 17 00:00:00 2001 From: Ivan Pechorin Date: Thu, 12 Sep 2024 04:35:16 +1200 Subject: [PATCH 037/230] properly retract v9.5.3 of redisotel and other extra packages (#3108) --- extra/rediscensus/go.mod | 4 ++++ extra/rediscmd/go.mod | 4 ++++ extra/redisotel/go.mod | 4 ++++ extra/redisprometheus/go.mod | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 7b99a888e3..33221d208c 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -17,3 +17,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ) + +retract ( + v9.5.3 // This version was accidentally released. +) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 2884faf9a7..7bc65f9ed0 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -14,3 +14,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) + +retract ( + v9.5.3 // This version was accidentally released. +) diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 12dd414a16..3a95b56e95 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -22,3 +22,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect golang.org/x/sys v0.16.0 // indirect ) + +retract ( + v9.5.3 // This version was accidentally released. +) diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index aedca634a4..342836007b 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -21,3 +21,7 @@ require ( golang.org/x/sys v0.4.0 // indirect google.golang.org/protobuf v1.33.0 // indirect ) + +retract ( + v9.5.3 // This version was accidentally released. +) From 83e7f563494b4efb97f00550aae44408a75755ea Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:26:10 +0300 Subject: [PATCH 038/230] Support Resp 3 Redis Search Unstable Mode (#3098) * Updated module version that points to retracted package version (#3074) * Updated module version that points to retracted package version * Updated testing image to latest * support raw parsing for problematic Redis Search types * Add UnstableResp3SearchModule to client options * Add tests for Resp3 Search unstable mode * Add tests for Resp3 Search unstable mode * Add readme note * Add words to spellcheck * Add UnstableResp3SearchModule check to assertStableCommand * Fix assertStableCommand logic * remove go.mod changes * Check panic occur on tests * rename method * update errors * Rename flag to UnstableResp3 --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: vladvildanov --- .github/wordlist.txt | 3 + README.md | 3 + command.go | 17 ++-- options.go | 3 + redis.go | 21 ++++- ring.go | 2 + search_commands.go | 39 +++++++++ search_test.go | 188 ++++++++++++++++++++++++++++++++++++++++++- sentinel.go | 3 + universal.go | 3 + 10 files changed, 273 insertions(+), 9 deletions(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index dceddff46a..c200c60b44 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -1,4 +1,5 @@ ACLs +APIs autoload autoloader autoloading @@ -46,9 +47,11 @@ runtime SHA sharding SETNAME +SpellCheck SSL struct stunnel +SynDump TCP TLS uri diff --git a/README.md b/README.md index c7951a4d4b..37714a9796 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,9 @@ rdb := redis.NewClient(&redis.Options{ }) ``` +#### Unstable RESP3 Structures for RediSearch Commands +When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. + ## Contributing Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! diff --git a/command.go b/command.go index 9ae97a95ae..4ced2979dc 100644 --- a/command.go +++ b/command.go @@ -40,7 +40,7 @@ type Cmder interface { readTimeout() *time.Duration readReply(rd *proto.Reader) error - + readRawReply(rd *proto.Reader) error SetErr(error) Err() error } @@ -122,11 +122,11 @@ func cmdString(cmd Cmder, val interface{}) string { //------------------------------------------------------------------------------ type baseCmd struct { - ctx context.Context - args []interface{} - err error - keyPos int8 - + ctx context.Context + args []interface{} + err error + keyPos int8 + rawVal interface{} _readTimeout *time.Duration } @@ -197,6 +197,11 @@ func (cmd *baseCmd) setReadTimeout(d time.Duration) { cmd._readTimeout = &d } +func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) { + cmd.rawVal, err = rd.ReadReply() + return err +} + //------------------------------------------------------------------------------ type Cmd struct { diff --git a/options.go b/options.go index 6ed693a0b0..8ba74ccd1a 100644 --- a/options.go +++ b/options.go @@ -153,6 +153,9 @@ type Options struct { // Add suffix to client name. Default is empty. IdentitySuffix string + + // Enable Unstable mode for Redis Search module with RESP3. + UnstableResp3 bool } func (opt *Options) init() { diff --git a/redis.go b/redis.go index 527afb677f..c8b5008090 100644 --- a/redis.go +++ b/redis.go @@ -412,6 +412,19 @@ func (c *baseClient) process(ctx context.Context, cmd Cmder) error { return lastErr } +func (c *baseClient) assertUnstableCommand(cmd Cmder) bool { + switch cmd.(type) { + case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd: + if c.opt.UnstableResp3 { + return true + } else { + panic("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3 . See the [README](https://github.com/redis/go-redis/blob/master/README.md) and the release notes for guidance.") + } + default: + return false + } +} + func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) { if attempt > 0 { if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { @@ -427,8 +440,12 @@ func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool atomic.StoreUint32(&retryTimeout, 1) return err } - - if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), cmd.readReply); err != nil { + readReplyFunc := cmd.readReply + // Apply unstable RESP3 search module. + if c.opt.Protocol != 2 && c.assertUnstableCommand(cmd) { + readReplyFunc = cmd.readRawReply + } + if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), readReplyFunc); err != nil { if cmd.readTimeout() == nil { atomic.StoreUint32(&retryTimeout, 1) } else { diff --git a/ring.go b/ring.go index 4ae00542ba..b402217344 100644 --- a/ring.go +++ b/ring.go @@ -100,6 +100,7 @@ type RingOptions struct { DisableIndentity bool IdentitySuffix string + UnstableResp3 bool } func (opt *RingOptions) init() { @@ -168,6 +169,7 @@ func (opt *RingOptions) clientOptions() *Options { DisableIndentity: opt.DisableIndentity, IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } diff --git a/search_commands.go b/search_commands.go index f5118c77e2..1a8a4cfef4 100644 --- a/search_commands.go +++ b/search_commands.go @@ -638,6 +638,14 @@ func (cmd *AggregateCmd) Result() (*FTAggregateResult, error) { return cmd.val, cmd.err } +func (cmd *AggregateCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *AggregateCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} + func (cmd *AggregateCmd) String() string { return cmdString(cmd, cmd.val) } @@ -1337,6 +1345,13 @@ func (cmd *FTInfoCmd) Val() FTInfoResult { return cmd.val } +func (cmd *FTInfoCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *FTInfoCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { n, err := rd.ReadMapLen() if err != nil { @@ -1447,6 +1462,14 @@ func (cmd *FTSpellCheckCmd) Val() []SpellCheckResult { return cmd.val } +func (cmd *FTSpellCheckCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} + func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { @@ -1628,6 +1651,14 @@ func (cmd *FTSearchCmd) Val() FTSearchResult { return cmd.val } +func (cmd *FTSearchCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *FTSearchCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} + func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { @@ -1904,6 +1935,14 @@ func (cmd *FTSynDumpCmd) Result() ([]FTSynDumpResult, error) { return cmd.val, cmd.err } +func (cmd *FTSynDumpCmd) RawVal() interface{} { + return cmd.rawVal +} + +func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) { + return cmd.rawVal, cmd.err +} + func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { termSynonymPairs, err := rd.ReadSlice() if err != nil { diff --git a/search_test.go b/search_test.go index 0e1a473b88..93859a4e72 100644 --- a/search_test.go +++ b/search_test.go @@ -18,11 +18,13 @@ func WaitForIndexing(c *redis.Client, index string) { return } time.Sleep(100 * time.Millisecond) + } else { + return } } } -var _ = Describe("RediSearch commands", Label("search"), func() { +var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -1415,3 +1417,187 @@ func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []str // Expect(results0["id"]).To(BeEquivalentTo("a")) // Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) // }) + +var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { + ctx := context.TODO() + var client *redis.Client + var client2 *redis.Client + + BeforeEach(func() { + client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true}) + client2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3}) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("should handle FTAggregate with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} + num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "637387878524969984") + client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "637387875859270016") + + options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult() + rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() + + Expect(err).NotTo(HaveOccurred()) + Expect(rawVal).To(BeEquivalentTo(res)) + results := res.(map[interface{}]interface{})["results"].([]interface{}) + Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). + To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). + To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + + // Test with UnstableResp3 false + Expect(func() { + options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} + rawRes, _ := client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult() + rawVal = client2.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() + Expect(rawRes).To(BeNil()) + Expect(rawVal).To(BeNil()) + }).Should(Panic()) + + }) + + It("should handle FTInfo with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftinfo"), func() { + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText, Sortable: true, NoStem: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resInfo, err := client.FTInfo(ctx, "idx1").RawResult() + Expect(err).NotTo(HaveOccurred()) + attributes := resInfo.(map[interface{}]interface{})["attributes"].([]interface{}) + flags := attributes[0].(map[interface{}]interface{})["flags"].([]interface{}) + Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM")) + + valInfo := client.FTInfo(ctx, "idx1").RawVal() + attributes = valInfo.(map[interface{}]interface{})["attributes"].([]interface{}) + flags = attributes[0].(map[interface{}]interface{})["flags"].([]interface{}) + Expect(flags).To(ConsistOf("SORTABLE", "NOSTEM")) + + // Test with UnstableResp3 false + Expect(func() { + rawResInfo, _ := client2.FTInfo(ctx, "idx1").RawResult() + rawValInfo := client2.FTInfo(ctx, "idx1").RawVal() + Expect(rawResInfo).To(BeNil()) + Expect(rawValInfo).To(BeNil()) + }).Should(Panic()) + }) + + It("should handle FTSpellCheck with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftspellcheck"), func() { + text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "f1", "some valid content", "f2", "this is sample text") + client.HSet(ctx, "doc2", "f1", "very important", "f2", "lorem ipsum") + + resSpellCheck, err := client.FTSpellCheck(ctx, "idx1", "impornant").RawResult() + valSpellCheck := client.FTSpellCheck(ctx, "idx1", "impornant").RawVal() + Expect(err).NotTo(HaveOccurred()) + Expect(valSpellCheck).To(BeEquivalentTo(resSpellCheck)) + results := resSpellCheck.(map[interface{}]interface{})["results"].(map[interface{}]interface{}) + Expect(results["impornant"].([]interface{})[0].(map[interface{}]interface{})["important"]).To(BeEquivalentTo(0.5)) + + // Test with UnstableResp3 false + Expect(func() { + rawResSpellCheck, _ := client2.FTSpellCheck(ctx, "idx1", "impornant").RawResult() + rawValSpellCheck := client2.FTSpellCheck(ctx, "idx1", "impornant").RawVal() + Expect(rawResSpellCheck).To(BeNil()) + Expect(rawValSpellCheck).To(BeNil()) + }).Should(Panic()) + }) + + It("should handle FTSearch with Unstable RESP3 Search Module and without stability", Label("search", "ftcreate", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{StopWords: []interface{}{"foo", "bar", "baz"}}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "hello world") + res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawResult() + val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{NoContent: true}).RawVal() + Expect(err).NotTo(HaveOccurred()) + Expect(val1).To(BeEquivalentTo(res1)) + totalResults := res1.(map[interface{}]interface{})["total_results"] + Expect(totalResults).To(BeEquivalentTo(int64(0))) + res2, err := client.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult() + Expect(err).NotTo(HaveOccurred()) + totalResults2 := res2.(map[interface{}]interface{})["total_results"] + Expect(totalResults2).To(BeEquivalentTo(int64(1))) + + // Test with UnstableResp3 false + Expect(func() { + rawRes2, _ := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawResult() + rawVal2 := client2.FTSearchWithArgs(ctx, "txt", "foo bar hello world", &redis.FTSearchOptions{NoContent: true}).RawVal() + Expect(rawRes2).To(BeNil()) + Expect(rawVal2).To(BeNil()) + }).Should(Panic()) + }) + It("should handle FTSynDump with Unstable RESP3 Search Module and without stability", Label("search", "ftsyndump"), func() { + text1 := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true}, text1, text2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + resSynUpdate, err := client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"boy", "child", "offspring"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"baby", "child"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynUpdate, err = client.FTSynUpdate(ctx, "idx1", "id1", []interface{}{"tree", "wood"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resSynUpdate).To(BeEquivalentTo("OK")) + + resSynDump, err := client.FTSynDump(ctx, "idx1").RawResult() + valSynDump := client.FTSynDump(ctx, "idx1").RawVal() + Expect(err).NotTo(HaveOccurred()) + Expect(valSynDump).To(BeEquivalentTo(resSynDump)) + Expect(resSynDump.(map[interface{}]interface{})["baby"]).To(BeEquivalentTo([]interface{}{"id1"})) + + // Test with UnstableResp3 false + Expect(func() { + rawResSynDump, _ := client2.FTSynDump(ctx, "idx1").RawResult() + rawValSynDump := client2.FTSynDump(ctx, "idx1").RawVal() + Expect(rawResSynDump).To(BeNil()) + Expect(rawValSynDump).To(BeNil()) + }).Should(Panic()) + }) + + It("should test not affected Resp 3 Search method - FTExplain", Label("search", "ftexplain"), func() { + text1 := &redis.FieldSchema{FieldName: "f1", FieldType: redis.SearchFieldTypeText} + text2 := &redis.FieldSchema{FieldName: "f2", FieldType: redis.SearchFieldTypeText} + text3 := &redis.FieldSchema{FieldName: "f3", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, text1, text2, text3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + res1, err := client.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res1).ToNot(BeEmpty()) + + // Test with UnstableResp3 false + Expect(func() { + res2, err := client2.FTExplain(ctx, "txt", "@f3:f3_val @f2:f2_val @f1:f1_val").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2).ToNot(BeEmpty()) + }).ShouldNot(Panic()) + }) +}) diff --git a/sentinel.go b/sentinel.go index 188f88494e..3156955445 100644 --- a/sentinel.go +++ b/sentinel.go @@ -82,6 +82,7 @@ type FailoverOptions struct { DisableIndentity bool IdentitySuffix string + UnstableResp3 bool } func (opt *FailoverOptions) clientOptions() *Options { @@ -119,6 +120,7 @@ func (opt *FailoverOptions) clientOptions() *Options { DisableIndentity: opt.DisableIndentity, IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } @@ -156,6 +158,7 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { DisableIndentity: opt.DisableIndentity, IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } diff --git a/universal.go b/universal.go index 275bef3d60..f4d2d75980 100644 --- a/universal.go +++ b/universal.go @@ -68,6 +68,7 @@ type UniversalOptions struct { DisableIndentity bool IdentitySuffix string + UnstableResp3 bool } // Cluster returns cluster options created from the universal options. @@ -160,6 +161,7 @@ func (o *UniversalOptions) Failover() *FailoverOptions { DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, } } @@ -203,6 +205,7 @@ func (o *UniversalOptions) Simple() *Options { DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, } } From b6e3877ff3bac4f78d49f41a2ce6eafc870d4d7e Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:45:38 +0100 Subject: [PATCH 039/230] Doc 4226 hash tces (#3106) * DOC-4226 hash examples for docs * DOC-4226 added hash scan examples * DOC-4226 fixed test output issues --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/hash_tutorial_test.go | 281 +++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 doctests/hash_tutorial_test.go diff --git a/doctests/hash_tutorial_test.go b/doctests/hash_tutorial_test.go new file mode 100644 index 0000000000..8b0b1ce9a7 --- /dev/null +++ b/doctests/hash_tutorial_test.go @@ -0,0 +1,281 @@ +// EXAMPLE: hash_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_set_get_all() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + // STEP_START set_get_all + hashFields := []string{ + "model", "Deimos", + "brand", "Ergonom", + "type", "Enduro bikes", + "price", "4972", + } + + res1, err := rdb.HSet(ctx, "bike:1", hashFields).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 4 + + res2, err := rdb.HGet(ctx, "bike:1", "model").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> Deimos + + res3, err := rdb.HGet(ctx, "bike:1", "price").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 4972 + + cmdReturn := rdb.HGetAll(ctx, "bike:1") + res4, err := cmdReturn.Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes] + + type BikeInfo struct { + Model string `redis:"model"` + Brand string `redis:"brand"` + Type string `redis:"type"` + Price int `redis:"price"` + } + + var res4a BikeInfo + + if err := cmdReturn.Scan(&res4a); err != nil { + panic(err) + } + + fmt.Printf("Model: %v, Brand: %v, Type: %v, Price: $%v\n", + res4a.Model, res4a.Brand, res4a.Type, res4a.Price) + // >>> Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972 + // STEP_END + + // Output: + // 4 + // Deimos + // 4972 + // map[brand:Ergonom model:Deimos price:4972 type:Enduro bikes] + // Model: Deimos, Brand: Ergonom, Type: Enduro bikes, Price: $4972 +} + +func ExampleClient_hmget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + hashFields := []string{ + "model", "Deimos", + "brand", "Ergonom", + "type", "Enduro bikes", + "price", "4972", + } + + _, err := rdb.HSet(ctx, "bike:1", hashFields).Result() + + if err != nil { + panic(err) + } + + // STEP_START hmget + cmdReturn := rdb.HMGet(ctx, "bike:1", "model", "price") + res5, err := cmdReturn.Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> [Deimos 4972] + + type BikeInfo struct { + Model string `redis:"model"` + Brand string `redis:"-"` + Type string `redis:"-"` + Price int `redis:"price"` + } + + var res5a BikeInfo + + if err := cmdReturn.Scan(&res5a); err != nil { + panic(err) + } + + fmt.Printf("Model: %v, Price: $%v\n", res5a.Model, res5a.Price) + // >>> Model: Deimos, Price: $4972 + // STEP_END + + // Output: + // [Deimos 4972] + // Model: Deimos, Price: $4972 +} + +func ExampleClient_hincrby() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + hashFields := []string{ + "model", "Deimos", + "brand", "Ergonom", + "type", "Enduro bikes", + "price", "4972", + } + + _, err := rdb.HSet(ctx, "bike:1", hashFields).Result() + + if err != nil { + panic(err) + } + + // STEP_START hincrby + res6, err := rdb.HIncrBy(ctx, "bike:1", "price", 100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> 5072 + + res7, err := rdb.HIncrBy(ctx, "bike:1", "price", -100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 4972 + // STEP_END + + // Output: + // 5072 + // 4972 +} + +func ExampleClient_incrby_get_mget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1:stats") + // REMOVE_END + + // STEP_START incrby_get_mget + res8, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 1 + + res9, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 2 + + res10, err := rdb.HIncrBy(ctx, "bike:1:stats", "rides", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 3 + + res11, err := rdb.HIncrBy(ctx, "bike:1:stats", "crashes", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 1 + + res12, err := rdb.HIncrBy(ctx, "bike:1:stats", "owners", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> 1 + + res13, err := rdb.HGet(ctx, "bike:1:stats", "rides").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> 3 + + res14, err := rdb.HMGet(ctx, "bike:1:stats", "crashes", "owners").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> [1 1] + // STEP_END + + // Output: + // 1 + // 2 + // 3 + // 1 + // 1 + // 3 + // [1 1] +} From 6f677c94b61705c91204be4bd0ac596143cc0888 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:54:42 +0100 Subject: [PATCH 040/230] DOC-4230 list doc code examples (#3110) * DOC-4230 list tutorial examples * DOC-4230 added missing printout comment * DOC-4230 added set datatype examples * Revert "DOC-4230 added set datatype examples" This reverts commit 4f208aaf7bf26c60f677ef872b794e0d641337cc. --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/list_tutorial_test.go | 766 +++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 doctests/list_tutorial_test.go diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go new file mode 100644 index 0000000000..908469ce05 --- /dev/null +++ b/doctests/list_tutorial_test.go @@ -0,0 +1,766 @@ +// EXAMPLE: list_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_queue() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START queue + res1, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 2 + + res3, err := rdb.RPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> bike:1 + + res4, err := rdb.RPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> bike:2 + // STEP_END + + // Output: + // 1 + // 2 + // bike:1 + // bike:2 +} + +func ExampleClient_stack() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START stack + res5, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> 1 + + res6, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> 2 + + res7, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> bike:2 + + res8, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> bike:1 + // STEP_END + + // Output: + // 1 + // 2 + // bike:2 + // bike:1 +} + +func ExampleClient_llen() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START llen + res9, err := rdb.LLen(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 0 + // STEP_END + + // Output: + // 0 +} + +func ExampleClient_lmove_lrange() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + rdb.Del(ctx, "bikes:finished") + // REMOVE_END + + // STEP_START lmove_lrange + res10, err := rdb.LPush(ctx, "bikes:repairs", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 1 + + res11, err := rdb.LPush(ctx, "bikes:repairs", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 2 + + res12, err := rdb.LMove(ctx, "bikes:repairs", "bikes:finished", "LEFT", "LEFT").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> bike:2 + + res13, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> [bike:1] + + res14, err := rdb.LRange(ctx, "bikes:finished", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> [bike:2] + // STEP_END + + // Output: + // 1 + // 2 + // bike:2 + // [bike:1] + // [bike:2] +} + +func ExampleClient_lpush_rpush() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START lpush_rpush + res15, err := rdb.RPush(ctx, "bikes:repairs", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> 1 + + res16, err := rdb.RPush(ctx, "bikes:repairs", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> 2 + + res17, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> 3 + + res18, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> [bike:important_bike bike:1 bike:2] + // STEP_END + + // Output: + // 1 + // 2 + // 3 + // [bike:important_bike bike:1 bike:2] +} + +func ExampleClient_variadic() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START variadic + res19, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> 3 + + res20, err := rdb.LPush(ctx, "bikes:repairs", "bike:important_bike", "bike:very_important_bike").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> 5 + + res21, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3] + // STEP_END + + // Output: + // 3 + // 5 + // [bike:very_important_bike bike:important_bike bike:1 bike:2 bike:3] +} + +func ExampleClient_lpop_rpop() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START lpop_rpop + res22, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> 3 + + res23, err := rdb.RPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> bike:3 + + res24, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) // >>> bike:1 + + res25, err := rdb.RPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) // >>> bike:2 + + res26, err := rdb.RPop(ctx, "bikes:repairs").Result() + + if err != nil { + fmt.Println(err) // >>> redis: nil + } + + fmt.Println(res26) // >>> + + // STEP_END + + // Output: + // 3 + // bike:3 + // bike:1 + // bike:2 + // redis: nil + // +} + +func ExampleClient_ltrim() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START ltrim + res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> 5 + + res28, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res28) // >>> OK + + res29, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res29) // >>> [bike:5 bike:4 bike:3] + // STEP_END + + // Output: + // 5 + // OK + // [bike:5 bike:4 bike:3] +} + +func ExampleClient_ltrim_end_of_list() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START ltrim_end_of_list + res30, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res30) // >>> 5 + + res31, err := rdb.LTrim(ctx, "bikes:repairs", -3, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res31) // >>> OK + + res32, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res32) // >>> [bike:3 bike:4 bike:5] + // STEP_END + + // Output: + // 5 + // OK + // [bike:3 bike:4 bike:5] +} + +func ExampleClient_brpop() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START brpop + res33, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res33) // >>> 2 + + res34, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res34) // >>> [bikes:repairs bike:2] + + res35, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res35) // >>> [bikes:repairs bike:1] + + res36, err := rdb.BRPop(ctx, 1, "bikes:repairs").Result() + + if err != nil { + fmt.Println(err) // >>> redis: nil + } + + fmt.Println(res36) // >>> [] + // STEP_END + + // Output: + // 2 + // [bikes:repairs bike:2] + // [bikes:repairs bike:1] + // redis: nil + // [] +} + +func ExampleClient_rule1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "new_bikes") + // REMOVE_END + + // STEP_START rule_1 + res37, err := rdb.Del(ctx, "new_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res37) // >>> 0 + + res38, err := rdb.LPush(ctx, "new_bikes", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res38) // >>> 3 + // STEP_END + + // Output: + // 0 + // 3 +} + +func ExampleClient_rule11() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "new_bikes") + // REMOVE_END + + // STEP_START rule_1.1 + res39, err := rdb.Set(ctx, "new_bikes", "bike:1", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res39) // >>> OK + + res40, err := rdb.Type(ctx, "new_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res40) // >>> string + + res41, err := rdb.LPush(ctx, "new_bikes", "bike:2", "bike:3").Result() + + if err != nil { + fmt.Println(err) + // >>> WRONGTYPE Operation against a key holding the wrong kind of value + } + + fmt.Println(res41) + // STEP_END + + // Output: + // OK + // string + // WRONGTYPE Operation against a key holding the wrong kind of value + // 0 +} + +func ExampleClient_rule2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START rule_2 + res42, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res42) // >>> 3 + + res43, err := rdb.Exists(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res43) // >>> 1 + + res44, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res44) // >>> bike:3 + + res45, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res45) // >>> bike:2 + + res46, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res46) // >>> bike:1 + + res47, err := rdb.Exists(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res47) // >>> 0 + // STEP_END + + // Output: + // 3 + // 1 + // bike:3 + // bike:2 + // bike:1 + // 0 +} + +func ExampleClient_rule3() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START rule_3 + res48, err := rdb.Del(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res48) // >>> 0 + + res49, err := rdb.LLen(ctx, "bikes:repairs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res49) // >>> 0 + + res50, err := rdb.LPop(ctx, "bikes:repairs").Result() + + if err != nil { + fmt.Println(err) // >>> redis: nil + } + + fmt.Println(res50) // >>> + // STEP_END + + // Output: + // 0 + // 0 + // redis: nil + // +} + +func ExampleClient_ltrim1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:repairs") + // REMOVE_END + + // STEP_START ltrim.1 + res51, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res51) // >>> 5 + + res52, err := rdb.LTrim(ctx, "bikes:repairs", 0, 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res52) // >>> OK + + res53, err := rdb.LRange(ctx, "bikes:repairs", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res53) // >>> [bike:5 bike:4 bike:3] + // STEP_END + + // Output: + // 5 + // OK + // [bike:5 bike:4 bike:3] +} From 1fc50a2854d9a8fbdadc025ef72b0f3bdf367e9b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:16:15 +0100 Subject: [PATCH 041/230] DOC-4231 added set datatype examples (#3111) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/sets_example_test.go | 442 ++++++++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 doctests/sets_example_test.go diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go new file mode 100644 index 0000000000..7446a2789d --- /dev/null +++ b/doctests/sets_example_test.go @@ -0,0 +1,442 @@ +// EXAMPLE: sets_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END +func ExampleClient_sadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + rdb.Del(ctx, "bikes:racing:usa") + // REMOVE_END + + // STEP_START sadd + res1, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 0 + + res3, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 2 + + res4, err := rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> 2 + // STEP_END + + // Output: + // 1 + // 0 + // 2 + // 2 +} + +func ExampleClient_sismember() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + rdb.Del(ctx, "bikes:racing:usa") + // REMOVE_END + + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result() + + if err != nil { + panic(err) + } + + // STEP_START sismember + res5, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> true + + res6, err := rdb.SIsMember(ctx, "bikes:racing:usa", "bike:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> false + // STEP_END + + // Output: + // true + // false +} + +func ExampleClient_sinter() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + rdb.Del(ctx, "bikes:racing:usa") + // REMOVE_END + + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result() + + if err != nil { + panic(err) + } + + // STEP_START sinter + res7, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> [bike:1] + // STEP_END + + // Output: + // [bike:1] +} + +func ExampleClient_scard() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + // REMOVE_END + + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + // STEP_START scard + res8, err := rdb.SCard(ctx, "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 3 + // STEP_END + + // Output: + // 3 +} + +func ExampleClient_saddsmembers() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + // REMOVE_END + + // STEP_START sadd_smembers + res9, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 3 + + res10, err := rdb.SMembers(ctx, "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> [bike:1 bike:2 bike:3] + // STEP_END + + // Output: + // 3 + // [bike:1 bike:2 bike:3] +} + +func ExampleClient_smismember() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + // REMOVE_END + + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + // STEP_START smismember + res11, err := rdb.SIsMember(ctx, "bikes:racing:france", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> true + + res12, err := rdb.SMIsMember(ctx, "bikes:racing:france", "bike:2", "bike:3", "bike:4").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> [true true false] + // STEP_END + + // Output: + // true + // [true true false] +} + +func ExampleClient_sdiff() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + rdb.Del(ctx, "bikes:racing:usa") + // REMOVE_END + + // STEP_START sdiff + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result() + + res13, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> [bike:2 bike:3] + // STEP_END + + // Output: + // [bike:2 bike:3] +} + +func ExampleClient_multisets() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + rdb.Del(ctx, "bikes:racing:usa") + rdb.Del(ctx, "bikes:racing:italy") + // REMOVE_END + + // STEP_START multisets + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.SAdd(ctx, "bikes:racing:usa", "bike:1", "bike:4").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.SAdd(ctx, "bikes:racing:italy", "bike:1", "bike:2", "bike:3", "bike:4").Result() + + if err != nil { + panic(err) + } + + res14, err := rdb.SInter(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> [bike:1] + + res15, err := rdb.SUnion(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4] + + res16, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> [] + + res17, err := rdb.SDiff(ctx, "bikes:racing:usa", "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [bike:4] + + res18, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> [bike:2 bike:3] + // STEP_END + + // Output: + // [bike:1] + // [bike:1 bike:2 bike:3 bike:4] + // [] + // [bike:4] + // [bike:2 bike:3] +} + +func ExampleClient_srem() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:racing:france") + // REMOVE_END + + // STEP_START srem + _, err := rdb.SAdd(ctx, "bikes:racing:france", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + + if err != nil { + panic(err) + } + + res19, err := rdb.SRem(ctx, "bikes:racing:france", "bike:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> 1 + + res20, err := rdb.SPop(ctx, "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> + + res21, err := rdb.SMembers(ctx, "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> + + res22, err := rdb.SRandMember(ctx, "bikes:racing:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> + // STEP_END + + // Testable examples not available because the test output + // is not deterministic. +} From e661d8e239b5b672ecee814068c4b289524c752e Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:26:16 +0100 Subject: [PATCH 042/230] DOC-4229 sorted set code examples (#3113) * DOC-4229 added sorted set doc examples * DOC-4229 wrapped long lines --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/ss_tutorial_test.go | 437 +++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 doctests/ss_tutorial_test.go diff --git a/doctests/ss_tutorial_test.go b/doctests/ss_tutorial_test.go new file mode 100644 index 0000000000..2a6924458d --- /dev/null +++ b/doctests/ss_tutorial_test.go @@ -0,0 +1,437 @@ +// EXAMPLE: ss_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END +func ExampleClient_zadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + // STEP_START zadd + res1, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 1 + + res3, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Sam-Bodden", Score: 8}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Ford", Score: 6}, + redis.Z{Member: "Prickett", Score: 14}, + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 4 + // STEP_END + + // Output: + // 1 + // 1 + // 4 +} + +func ExampleClient_zrange() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Sam-Bodden", Score: 8}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Ford", Score: 6}, + redis.Z{Member: "Prickett", Score: 14}, + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START zrange + res4, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> [Ford Sam-Bodden Norem Royce Castilla Prickett] + + res5, err := rdb.ZRevRange(ctx, "racer_scores", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> [Prickett Castilla Royce Norem Sam-Bodden Ford] + // STEP_END + + // Output: + // [Ford Sam-Bodden Norem Royce Castilla Prickett] + // [Prickett Castilla Royce Norem Sam-Bodden Ford] +} + +func ExampleClient_zrangewithscores() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Sam-Bodden", Score: 8}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Ford", Score: 6}, + redis.Z{Member: "Prickett", Score: 14}, + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START zrange_withscores + res6, err := rdb.ZRangeWithScores(ctx, "racer_scores", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) + // >>> [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}] + // STEP_END + + // Output: + // [{6 Ford} {8 Sam-Bodden} {10 Norem} {10 Royce} {12 Castilla} {14 Prickett}] +} + +func ExampleClient_zrangebyscore() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Sam-Bodden", Score: 8}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Ford", Score: 6}, + redis.Z{Member: "Prickett", Score: 14}, + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START zrangebyscore + res7, err := rdb.ZRangeByScore(ctx, "racer_scores", + &redis.ZRangeBy{Min: "-inf", Max: "10"}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) + // >>> [Ford Sam-Bodden Norem Royce] + // STEP_END + + // Output: + // [Ford Sam-Bodden Norem Royce] +} + +func ExampleClient_zremrangebyscore() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Sam-Bodden", Score: 8}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Ford", Score: 6}, + redis.Z{Member: "Prickett", Score: 14}, + redis.Z{Member: "Castilla", Score: 12}, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START zremrangebyscore + res8, err := rdb.ZRem(ctx, "racer_scores", "Castilla").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 1 + + res9, err := rdb.ZRemRangeByScore(ctx, "racer_scores", "-inf", "9").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 2 + + res10, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) + // >>> [Norem Royce Prickett] + // STEP_END + + // Output: + // 1 + // 2 + // [Norem Royce Prickett] +} + +func ExampleClient_zrank() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 10}, + redis.Z{Member: "Royce", Score: 10}, + redis.Z{Member: "Prickett", Score: 14}, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START zrank + res11, err := rdb.ZRank(ctx, "racer_scores", "Norem").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 0 + + res12, err := rdb.ZRevRank(ctx, "racer_scores", "Norem").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> 2 + // STEP_END + + // Output: + // 0 + // 2 +} + +func ExampleClient_zaddlex() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + _, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 0}, + redis.Z{Member: "Royce", Score: 0}, + redis.Z{Member: "Prickett", Score: 0}, + ).Result() + + // STEP_START zadd_lex + res13, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Norem", Score: 0}, + redis.Z{Member: "Sam-Bodden", Score: 0}, + redis.Z{Member: "Royce", Score: 0}, + redis.Z{Member: "Ford", Score: 0}, + redis.Z{Member: "Prickett", Score: 0}, + redis.Z{Member: "Castilla", Score: 0}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> 3 + + res14, err := rdb.ZRange(ctx, "racer_scores", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) + // >>> [Castilla Ford Norem Prickett Royce Sam-Bodden] + + res15, err := rdb.ZRangeByLex(ctx, "racer_scores", &redis.ZRangeBy{ + Min: "[A", Max: "[L", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [Castilla Ford] + // STEP_END + + // Output: + // 3 + // [Castilla Ford Norem Prickett Royce Sam-Bodden] + // [Castilla Ford] +} + +func ExampleClient_leaderboard() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_scores") + // REMOVE_END + + // STEP_START leaderboard + res16, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Wood", Score: 100}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> 1 + + res17, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Henshaw", Score: 100}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> 1 + + res18, err := rdb.ZAdd(ctx, "racer_scores", + redis.Z{Member: "Henshaw", Score: 150}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> 0 + + res19, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Wood").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> 150 + + res20, err := rdb.ZIncrBy(ctx, "racer_scores", 50, "Henshaw").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> 200 + // STEP_END + + // Output: + // 1 + // 1 + // 0 + // 150 + // 200 +} From 8a1da5819427f2b6a524e6ca0ef35944b8b2b24f Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:54:20 +0100 Subject: [PATCH 043/230] DOC-4241 added t-digest examples (#3123) --- doctests/tdigest_tutorial_test.go | 251 ++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 doctests/tdigest_tutorial_test.go diff --git a/doctests/tdigest_tutorial_test.go b/doctests/tdigest_tutorial_test.go new file mode 100644 index 0000000000..7589b0ec89 --- /dev/null +++ b/doctests/tdigest_tutorial_test.go @@ -0,0 +1,251 @@ +// EXAMPLE: tdigest_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_tdigstart() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages", "bikes:sales") + // REMOVE_END + + // STEP_START tdig_start + res1, err := rdb.TDigestCreate(ctx, "bikes:sales").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TDigestAdd(ctx, "bikes:sales", 21).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> OK + + res3, err := rdb.TDigestAdd(ctx, "bikes:sales", + 150, 95, 75, 34, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> OK + + // STEP_END + + // Output: + // OK + // OK + // OK +} + +func ExampleClient_tdigcdf() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages", "bikes:sales") + // REMOVE_END + + // STEP_START tdig_cdf + res4, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> OK + + res5, err := rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> OK + + res6, err := rdb.TDigestRank(ctx, "racer_ages", 50).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> [7] + + res7, err := rdb.TDigestRank(ctx, "racer_ages", 50, 40).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> [7 4] + // STEP_END + + // Output: + // OK + // OK + // [7] + // [7 4] +} + +func ExampleClient_tdigquant() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_quant + res8, err := rdb.TDigestQuantile(ctx, "racer_ages", 0.5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> [44.2] + + res9, err := rdb.TDigestByRank(ctx, "racer_ages", 4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [42.63] + // STEP_END + + // Output: + // [44.2] + // [42.63] +} + +func ExampleClient_tdigmin() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_min + res10, err := rdb.TDigestMin(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 19.27 + + res11, err := rdb.TDigestMax(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 85.71 + // STEP_END + + // Output: + // 19.27 + // 85.71 +} + +func ExampleClient_tdigreset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_reset + res12, err := rdb.TDigestReset(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> OK + // STEP_END + + // Output: + // OK +} From d1f6595343e33e307db6c7564a6d9aebc60a6209 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:03:54 +0100 Subject: [PATCH 044/230] DOC-4234 added bitmap examples (#3124) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/bitmap_tutorial_test.go | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 doctests/bitmap_tutorial_test.go diff --git a/doctests/bitmap_tutorial_test.go b/doctests/bitmap_tutorial_test.go new file mode 100644 index 0000000000..dbfc247ac9 --- /dev/null +++ b/doctests/bitmap_tutorial_test.go @@ -0,0 +1,92 @@ +// EXAMPLE: bitmap_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_ping() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "pings:2024-01-01-00:00") + // REMOVE_END + + // STEP_START ping + res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 0 + + res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 1 + + res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 0 + // STEP_END + + // Output: + // 0 + // 1 + // 0 +} + +func ExampleClient_bitcount() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() + + if err != nil { + panic(err) + } + // REMOVE_END + + // STEP_START bitcount + res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00", + &redis.BitCount{ + Start: 0, + End: 456, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> 1 + // STEP_END + + // Output: + // 1 +} From 5946de14ade3887f123f0b084c77579a42b3df43 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:24:21 +0100 Subject: [PATCH 045/230] DOC-4228 JSON code examples (#3114) * DOC-4228 added JSON code examples * DOC-4228 example and formatting corrections --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/json_tutorial_test.go | 1149 ++++++++++++++++++++++++++++++++ 1 file changed, 1149 insertions(+) create mode 100644 doctests/json_tutorial_test.go diff --git a/doctests/json_tutorial_test.go b/doctests/json_tutorial_test.go new file mode 100644 index 0000000000..4e9787330a --- /dev/null +++ b/doctests/json_tutorial_test.go @@ -0,0 +1,1149 @@ +// EXAMPLE: json_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END +func ExampleClient_setget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike") + // REMOVE_END + + // STEP_START set_get + res1, err := rdb.JSONSet(ctx, "bike", "$", + "\"Hyperion\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.JSONGet(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> ["Hyperion"] + + res3, err := rdb.JSONType(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [[string]] + // STEP_END + + // Output: + // OK + // ["Hyperion"] + // [[string]] +} + +func ExampleClient_str() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bike", "$", + "\"Hyperion\"", + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START str + res4, err := rdb.JSONStrLen(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res4[0]) // >>> 8 + + res5, err := rdb.JSONStrAppend(ctx, "bike", "$", "\" (Enduro bikes)\"").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res5[0]) // >>> 23 + + res6, err := rdb.JSONGet(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> ["Hyperion (Enduro bikes)"] + // STEP_END + + // Output: + // 8 + // 23 + // ["Hyperion (Enduro bikes)"] +} + +func ExampleClient_num() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "crashes") + // REMOVE_END + + // STEP_START num + res7, err := rdb.JSONSet(ctx, "crashes", "$", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> OK + + res8, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> [1] + + res9, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1.5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [2.5] + + res10, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", -0.75).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> [1.75] + // STEP_END + + // Output: + // OK + // [1] + // [2.5] + // [1.75] +} + +func ExampleClient_arr() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "newbike") + // REMOVE_END + + // STEP_START arr + res11, err := rdb.JSONSet(ctx, "newbike", "$", + []interface{}{ + "Deimos", + map[string]interface{}{"crashes": 0}, + nil, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> OK + + res12, err := rdb.JSONGet(ctx, "newbike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> [["Deimos",{"crashes":0},null]] + + res13, err := rdb.JSONGet(ctx, "newbike", "$[1].crashes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> [0] + + res14, err := rdb.JSONDel(ctx, "newbike", "$.[-1]").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> 1 + + res15, err := rdb.JSONGet(ctx, "newbike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [["Deimos",{"crashes":0}]] + // STEP_END + + // Output: + // OK + // [["Deimos",{"crashes":0},null]] + // [0] + // 1 + // [["Deimos",{"crashes":0}]] +} + +func ExampleClient_arr2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "riders") + // REMOVE_END + + // STEP_START arr2 + res16, err := rdb.JSONSet(ctx, "riders", "$", []interface{}{}).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> OK + + res17, err := rdb.JSONArrAppend(ctx, "riders", "$", "\"Norem\"").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [1] + + res18, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> [["Norem"]] + + res19, err := rdb.JSONArrInsert(ctx, "riders", "$", 1, + "\"Prickett\"", "\"Royce\"", "\"Castilla\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // [3] + + res20, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> [["Norem", "Prickett", "Royce", "Castilla"]] + + rangeStop := 1 + + res21, err := rdb.JSONArrTrimWithArgs(ctx, "riders", "$", + &redis.JSONArrTrimArgs{Start: 1, Stop: &rangeStop}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> [1] + + res22, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> [["Prickett"]] + + res23, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> [["Prickett"]] + + res24, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) // [] + // STEP_END + + // Output: + // OK + // [1] + // [["Norem"]] + // [4] + // [["Norem","Prickett","Royce","Castilla"]] + // [1] + // [["Prickett"]] + // ["Prickett"] + // [] +} + +func ExampleClient_obj() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + // STEP_START obj + res25, err := rdb.JSONSet(ctx, "bike:1", "$", + map[string]interface{}{ + "model": "Deimos", + "brand": "Ergonom", + "price": 4972, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) // >>> OK + + res26, err := rdb.JSONObjLen(ctx, "bike:1", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res26[0]) // >>> 3 + + res27, err := rdb.JSONObjKeys(ctx, "bike:1", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> [brand model price] + // STEP_END + + // Output: + // OK + // 3 + // [[brand model price]] +} + +var inventory_json = map[string]interface{}{ + "inventory": map[string]interface{}{ + "mountain_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic " + + "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " + + "gives plenty of gear range to tackle hills and there\u2019s room for " + + "mudguards and a rack too. This is the bike for the rider who wants " + + "trail manners with low fuss ownership.", + "price": 1920, + "specs": map[string]interface{}{"material": "carbon", "weight": 13.1}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike " + + "impressed our testers and is the best all-around trail bike we've " + + "ever tested. The Shimano gear system effectively does away with an " + + "external cassette, so is super low maintenance in terms of wear " + + "and tear. All in all it's an impressive package for the price, " + + "making it very competitive.", + "price": 2072, + "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9}, + "colors": []interface{}{"black", "white"}, + }, + map[string]interface{}{ + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first experience " + + "on tracks and easy cruising through forests and fields. A set of " + + "powerful Shimano hydraulic disc brakes provide ample stopping " + + "ability. If you're after a budget option, this is one of the best " + + "bikes you could get.", + "price": 3264, + "specs": map[string]interface{}{"material": "alloy", "weight": 13.8}, + }, + }, + "commuter_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:4", + "model": "Salacia", + "description": "This bike is a great option for anyone who just " + + "wants a bike to get about on With a slick-shifting Claris gears " + + "from Shimano\u2019s, this is a bike which doesn\u2019t break the " + + "bank and delivers craved performance. It\u2019s for the rider " + + "who wants both efficiency and capability.", + "price": 1475, + "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:5", + "model": "Mimas", + "description": "A real joy to ride, this bike got very high " + + "scores in last years Bike of the year report. The carefully " + + "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " + + "easy-on-the-legs bottom gear for climbing, and the high-quality " + + "Vittoria Zaffiro tires give balance and grip.It includes " + + "a low-step frame , our memory foam seat, bump-resistant shocks and " + + "conveniently placed thumb throttle. Put it all together and you " + + "get a bike that helps redefine what can be done for this price.", + "price": 3941, + "specs": map[string]interface{}{"material": "alloy", "weight": 11.6}, + }, + }, + }, +} + +func ExampleClient_setbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + // STEP_START set_bikes + var inventory_json = map[string]interface{}{ + "inventory": map[string]interface{}{ + "mountain_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic " + + "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " + + "gives plenty of gear range to tackle hills and there\u2019s room for " + + "mudguards and a rack too. This is the bike for the rider who wants " + + "trail manners with low fuss ownership.", + "price": 1920, + "specs": map[string]interface{}{"material": "carbon", "weight": 13.1}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike " + + "impressed our testers and is the best all-around trail bike we've " + + "ever tested. The Shimano gear system effectively does away with an " + + "external cassette, so is super low maintenance in terms of wear " + + "and tear. All in all it's an impressive package for the price, " + + "making it very competitive.", + "price": 2072, + "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9}, + "colors": []interface{}{"black", "white"}, + }, + map[string]interface{}{ + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first experience " + + "on tracks and easy cruising through forests and fields. A set of " + + "powerful Shimano hydraulic disc brakes provide ample stopping " + + "ability. If you're after a budget option, this is one of the best " + + "bikes you could get.", + "price": 3264, + "specs": map[string]interface{}{"material": "alloy", "weight": 13.8}, + }, + }, + "commuter_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:4", + "model": "Salacia", + "description": "This bike is a great option for anyone who just " + + "wants a bike to get about on With a slick-shifting Claris gears " + + "from Shimano\u2019s, this is a bike which doesn\u2019t break the " + + "bank and delivers craved performance. It\u2019s for the rider " + + "who wants both efficiency and capability.", + "price": 1475, + "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:5", + "model": "Mimas", + "description": "A real joy to ride, this bike got very high " + + "scores in last years Bike of the year report. The carefully " + + "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " + + "easy-on-the-legs bottom gear for climbing, and the high-quality " + + "Vittoria Zaffiro tires give balance and grip.It includes " + + "a low-step frame , our memory foam seat, bump-resistant shocks and " + + "conveniently placed thumb throttle. Put it all together and you " + + "get a bike that helps redefine what can be done for this price.", + "price": 3941, + "specs": map[string]interface{}{"material": "alloy", "weight": 11.6}, + }, + }, + }, + } + + res1, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_getbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_bikes + res2, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory", + &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "}, + "$.inventory.*", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) + // >>> + // [ + // [ + // { + // "colors": [ + // "black", + // "silver" + // ... + // STEP_END + + // Output: + // [ + // [ + // { + // "colors": [ + // "black", + // "silver" + // ], + // "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance. It’s for the rider who wants both efficiency and capability.", + // "id": "bike:4", + // "model": "Salacia", + // "price": 1475, + // "specs": { + // "material": "aluminium", + // "weight": 16.6 + // } + // }, + // { + // "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.", + // "id": "bike:5", + // "model": "Mimas", + // "price": 3941, + // "specs": { + // "material": "alloy", + // "weight": 11.6 + // } + // } + // ], + // [ + // { + // "colors": [ + // "black", + // "silver" + // ], + // "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.", + // "id": "bike:1", + // "model": "Phoebe", + // "price": 1920, + // "specs": { + // "material": "carbon", + // "weight": 13.1 + // } + // }, + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + // "id": "bike:2", + // "model": "Quaoar", + // "price": 2072, + // "specs": { + // "material": "aluminium", + // "weight": 7.9 + // } + // }, + // { + // "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + // "id": "bike:3", + // "model": "Weywot", + // "price": 3264, + // "specs": { + // "material": "alloy", + // "weight": 13.8 + // } + // } + // ] + // ] +} + +func ExampleClient_getmtnbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_mtnbikes + res3, err := rdb.JSONGet(ctx, "bikes:inventory", + "$.inventory.mountain_bikes[*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) + // >>> ["Phoebe","Quaoar","Weywot"] + + res4, err := rdb.JSONGet(ctx, + "bikes:inventory", "$.inventory[\"mountain_bikes\"][*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> ["Phoebe","Quaoar","Weywot"] + + res5, err := rdb.JSONGet(ctx, + "bikes:inventory", "$..mountain_bikes[*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> ["Phoebe","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Phoebe","Quaoar","Weywot"] + // ["Phoebe","Quaoar","Weywot"] + // ["Phoebe","Quaoar","Weywot"] +} + +func ExampleClient_getmodels() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_models + res6, err := rdb.JSONGet(ctx, "bikes:inventory", "$..model").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> ["Salacia","Mimas","Phoebe","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Salacia","Mimas","Phoebe","Quaoar","Weywot"] +} + +func ExampleClient_get2mtnbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get2mtnbikes + res7, err := rdb.JSONGet(ctx, "bikes:inventory", "$..mountain_bikes[0:2].model").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> ["Phoebe","Quaoar"] + // STEP_END + + // Output: + // ["Phoebe","Quaoar"] +} + +func ExampleClient_filter1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter1 + res8, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory", + &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "}, + "$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) + // >>> + // [ + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year + // ... + // STEP_END + + // Output: + // [ + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + // "id": "bike:2", + // "model": "Quaoar", + // "price": 2072, + // "specs": { + // "material": "aluminium", + // "weight": 7.9 + // } + // } + // ] +} + +func ExampleClient_filter2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter2 + res9, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$..[?(@.specs.material == 'alloy')].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> ["Mimas","Weywot"] + // STEP_END + + // Output: + // ["Mimas","Weywot"] +} + +func ExampleClient_filter3() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter3 + res10, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$..[?(@.specs.material =~ '(?i)al')].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> ["Salacia","Mimas","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Salacia","Mimas","Quaoar","Weywot"] +} + +func ExampleClient_filter4() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter4 + res11, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[0].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> OK + + res12, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[1].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> OK + + res13, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[2].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> OK + + res14, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> ["Quaoar","Weywot"] + // STEP_END + + // Output: + // OK + // OK + // OK + // ["Quaoar","Weywot"] +} + +func ExampleClient_updatebikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_bikes + res15, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [1475,3941,1920,2072,3264] + + res16, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", -100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> [1375,3841,1820,1972,3164] + + res17, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", 100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [1475,3941,1920,2072,3264] + // STEP_END + + // Output: + // [1475,3941,1920,2072,3264] + // [1375,3841,1820,1972,3164] + // [1475,3941,1920,2072,3264] +} + +func ExampleClient_updatefilters1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_filters1 + res18, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.*[?(@.price<2000)].price", + 1500, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> OK + + res19, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> [1500,3941,1500,2072,3264] + // STEP_END + + // Output: + // OK + // [1500,3941,1500,2072,3264] +} + +func ExampleClient_updatefilters2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_filters2 + res20, err := rdb.JSONArrAppend(ctx, + "bikes:inventory", + "$.inventory.*[?(@.price<2000)].colors", + "\"pink\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> [3 3] + + res21, err := rdb.JSONGet(ctx, "bikes:inventory", "$..[*].colors").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) + // >>> [["black","silver","pink"],["black","silver","pink"],["black","white"]] + // STEP_END + + // Output: + // [3 3] + // [["black","silver","pink"],["black","silver","pink"],["black","white"]] +} From 9c8a0a9c29c1a64a2a3bc5dd4e0a743bd7ab1619 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:41:54 +0100 Subject: [PATCH 046/230] DOC-4237 added Bloom filter examples (#3115) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/bf_tutorial_test.go | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 doctests/bf_tutorial_test.go diff --git a/doctests/bf_tutorial_test.go b/doctests/bf_tutorial_test.go new file mode 100644 index 0000000000..67545f1d5e --- /dev/null +++ b/doctests/bf_tutorial_test.go @@ -0,0 +1,83 @@ +// EXAMPLE: bf_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_bloom() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:models") + // REMOVE_END + + // STEP_START bloom + res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + res4, err := rdb.BFMAdd(ctx, "bikes:models", + "Rocky Mountain Racer", + "Cloudy City Cruiser", + "Windy City Wippet", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [true true true] + + res5, err := rdb.BFMExists(ctx, "bikes:models", + "Rocky Mountain Racer", + "Cloudy City Cruiser", + "Windy City Wippet", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> [true true true] + // STEP_END + + // Output: + // OK + // true + // true + // [true true true] + // [true true true] +} From c8e4a1eebf1663e23c3ff8daceb27e93ef31c39e Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:11:59 +0300 Subject: [PATCH 047/230] Fix Flaky Test: should handle FTAggregate with Unstable RESP3 Search Module and without stability (#3135) --- go.mod | 2 +- search_test.go | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index bd13d74530..c1d9037acc 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. + v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) diff --git a/search_test.go b/search_test.go index 93859a4e72..efdc6bb1e5 100644 --- a/search_test.go +++ b/search_test.go @@ -1446,16 +1446,18 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult() - rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() - - Expect(err).NotTo(HaveOccurred()) - Expect(rawVal).To(BeEquivalentTo(res)) results := res.(map[interface{}]interface{})["results"].([]interface{}) Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() + rawValResults := rawVal.(map[interface{}]interface{})["results"].([]interface{}) + Expect(err).NotTo(HaveOccurred()) + Expect(rawValResults[0]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1]))) + Expect(rawValResults[1]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1]))) + // Test with UnstableResp3 false Expect(func() { options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} From c64e94717a2f0eff2578abc1d629ca2ff8735911 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 7 Oct 2024 10:23:11 +0100 Subject: [PATCH 048/230] Remove direct read from TLS underlying conn (#3138) --- internal/pool/conn_check.go | 5 ----- internal/pool/conn_check_test.go | 18 ------------------ 2 files changed, 23 deletions(-) diff --git a/internal/pool/conn_check.go b/internal/pool/conn_check.go index 07c261c2bb..83190d3948 100644 --- a/internal/pool/conn_check.go +++ b/internal/pool/conn_check.go @@ -3,7 +3,6 @@ package pool import ( - "crypto/tls" "errors" "io" "net" @@ -17,10 +16,6 @@ func connCheck(conn net.Conn) error { // Reset previous timeout. _ = conn.SetDeadline(time.Time{}) - // Check if tls.Conn. - if c, ok := conn.(*tls.Conn); ok { - conn = c.NetConn() - } sysConn, ok := conn.(syscall.Conn) if !ok { return nil diff --git a/internal/pool/conn_check_test.go b/internal/pool/conn_check_test.go index 2149933390..2ade8a0b97 100644 --- a/internal/pool/conn_check_test.go +++ b/internal/pool/conn_check_test.go @@ -3,7 +3,6 @@ package pool import ( - "crypto/tls" "net" "net/http/httptest" "time" @@ -15,17 +14,12 @@ import ( var _ = Describe("tests conn_check with real conns", func() { var ts *httptest.Server var conn net.Conn - var tlsConn *tls.Conn var err error BeforeEach(func() { ts = httptest.NewServer(nil) conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second) Expect(err).NotTo(HaveOccurred()) - tlsTestServer := httptest.NewUnstartedServer(nil) - tlsTestServer.StartTLS() - tlsConn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, tlsTestServer.Listener.Addr().Network(), tlsTestServer.Listener.Addr().String(), &tls.Config{InsecureSkipVerify: true}) - Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { @@ -39,23 +33,11 @@ var _ = Describe("tests conn_check with real conns", func() { Expect(connCheck(conn)).To(HaveOccurred()) }) - It("good tls conn check", func() { - Expect(connCheck(tlsConn)).NotTo(HaveOccurred()) - - Expect(tlsConn.Close()).NotTo(HaveOccurred()) - Expect(connCheck(tlsConn)).To(HaveOccurred()) - }) - It("bad conn check", func() { Expect(conn.Close()).NotTo(HaveOccurred()) Expect(connCheck(conn)).To(HaveOccurred()) }) - It("bad tls conn check", func() { - Expect(tlsConn.Close()).NotTo(HaveOccurred()) - Expect(connCheck(tlsConn)).To(HaveOccurred()) - }) - It("check conn deadline", func() { Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred()) time.Sleep(time.Millisecond * 10) From fab9d4b79ffd001bf6d27b9c4851c649e0239dfc Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 06:56:44 +0100 Subject: [PATCH 049/230] DOC-4238 added Cuckoo filter examples (#3116) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/cuckoo_tutorial_test.go | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/cuckoo_tutorial_test.go diff --git a/doctests/cuckoo_tutorial_test.go b/doctests/cuckoo_tutorial_test.go new file mode 100644 index 0000000000..08a503b10e --- /dev/null +++ b/doctests/cuckoo_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: cuckoo_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cuckoo() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:models") + // REMOVE_END + + // STEP_START cuckoo + res1, err := rdb.CFReserve(ctx, "bikes:models", 1000000).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.CFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.CFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + res4, err := rdb.CFExists(ctx, "bikes:models", "Terrible Bike Name").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> false + + res5, err := rdb.CFDel(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> true + // STEP_END + + // Output: + // OK + // true + // true + // false + // true +} From 30f193d5355522fb146c051e56ccfa918492a866 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 07:14:34 +0100 Subject: [PATCH 050/230] DOC-4236 added HyperLogLog examples (#3117) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/hll_tutorial_test.go | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/hll_tutorial_test.go diff --git a/doctests/hll_tutorial_test.go b/doctests/hll_tutorial_test.go new file mode 100644 index 0000000000..57e78d1081 --- /dev/null +++ b/doctests/hll_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: hll_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_pfadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes") + // REMOVE_END + + // STEP_START pfadd + res1, err := rdb.PFAdd(ctx, "bikes", "Hyperion", "Deimos", "Phoebe", "Quaoar").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // 1 + + res2, err := rdb.PFCount(ctx, "bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // 4 + + res3, err := rdb.PFAdd(ctx, "commuter_bikes", "Salacia", "Mimas", "Quaoar").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // 1 + + res4, err := rdb.PFMerge(ctx, "all_bikes", "bikes", "commuter_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // OK + + res5, err := rdb.PFCount(ctx, "all_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // 6 + // STEP_END + + // Output: + // 1 + // 4 + // 1 + // OK + // 6 +} From b1ae5f658f3ae2defcd20bc0b2b1c8ca04d7136a Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:24:47 +0100 Subject: [PATCH 051/230] DOC-4239 added Count-min sketch examples (#3118) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cms_tutorial_test.go | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 doctests/cms_tutorial_test.go diff --git a/doctests/cms_tutorial_test.go b/doctests/cms_tutorial_test.go new file mode 100644 index 0000000000..ade1fa93da --- /dev/null +++ b/doctests/cms_tutorial_test.go @@ -0,0 +1,84 @@ +// EXAMPLE: cms_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cms() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:profit") + // REMOVE_END + + // STEP_START cms + res1, err := rdb.CMSInitByProb(ctx, "bikes:profit", 0.001, 0.002).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.CMSIncrBy(ctx, "bikes:profit", + "Smoky Mountain Striker", 100, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [100] + + res3, err := rdb.CMSIncrBy(ctx, "bikes:profit", + "Rocky Mountain Racer", 200, + "Cloudy City Cruiser", 150, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [200 150] + + res4, err := rdb.CMSQuery(ctx, "bikes:profit", + "Smoky Mountain Striker", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [100] + + res5, err := rdb.CMSInfo(ctx, "bikes:profit").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Width: %v, Depth: %v, Count: %v", + res5.Width, res5.Depth, res5.Count) + // >>> Width: 2000, Depth: 9, Count: 450 + // STEP_END + + // Output: + // OK + // [100] + // [200 150] + // [100] + // Width: 2000, Depth: 9, Count: 450 +} From 6bf55afdf2e23fbb61b5d15ea4bfcb5d442a47dd Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:31:14 +0100 Subject: [PATCH 052/230] DOC-4235 added bitfield examples (#3125) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/bitfield_tutorial_test.go | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 doctests/bitfield_tutorial_test.go diff --git a/doctests/bitfield_tutorial_test.go b/doctests/bitfield_tutorial_test.go new file mode 100644 index 0000000000..04fcb35f24 --- /dev/null +++ b/doctests/bitfield_tutorial_test.go @@ -0,0 +1,79 @@ +// EXAMPLE: bitfield_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_bf() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1:stats") + // REMOVE_END + + // STEP_START bf + res1, err := rdb.BitField(ctx, "bike:1:stats", + "set", "u32", "#0", "1000", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> [0] + + res2, err := rdb.BitField(ctx, + "bike:1:stats", + "incrby", "u32", "#0", "-50", + "incrby", "u32", "#1", "1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [950 1] + + res3, err := rdb.BitField(ctx, + "bike:1:stats", + "incrby", "u32", "#0", "500", + "incrby", "u32", "#1", "1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [1450 2] + + res4, err := rdb.BitField(ctx, "bike:1:stats", + "get", "u32", "#0", + "get", "u32", "#1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [1450 2] + // STEP_END + + // Output: + // [0] + // [950 1] + // [1450 2] + // [1450 2] +} From 392b9aeba0a19a638c9417a5e77d6fcfed425745 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:33:46 +0100 Subject: [PATCH 053/230] DOC-4240 added Top-K examples (#3119) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/topk_tutorial_test.go | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/topk_tutorial_test.go diff --git a/doctests/topk_tutorial_test.go b/doctests/topk_tutorial_test.go new file mode 100644 index 0000000000..2d1fe7fc22 --- /dev/null +++ b/doctests/topk_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: topk_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_topk() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:keywords") + // REMOVE_END + + // STEP_START topk + res1, err := rdb.TopKReserve(ctx, "bikes:keywords", 5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TopKAdd(ctx, "bikes:keywords", + "store", + "seat", + "handlebars", + "handles", + "pedals", + "tires", + "store", + "seat", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [ handlebars ] + + res3, err := rdb.TopKList(ctx, "bikes:keywords").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // [store seat pedals tires handles] + + res4, err := rdb.TopKQuery(ctx, "bikes:keywords", "store", "handlebars").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // [true false] + // STEP_END + + // Output: + // OK + // [ handlebars ] + // [store seat pedals tires handles] + // [true false] +} From 3d780f7ac404a63718be904dc21ca1f7a66cb800 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:36:42 +0100 Subject: [PATCH 054/230] DOC-4233 added geospatial examples (#3126) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/geo_tutorial_test.go | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 doctests/geo_tutorial_test.go diff --git a/doctests/geo_tutorial_test.go b/doctests/geo_tutorial_test.go new file mode 100644 index 0000000000..051db623b4 --- /dev/null +++ b/doctests/geo_tutorial_test.go @@ -0,0 +1,139 @@ +// EXAMPLE: geo_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:rentable") + // REMOVE_END + + // STEP_START geoadd + res1, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.27652, + Latitude: 37.805186, + Name: "station:1", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2674626, + Latitude: 37.8062344, + Name: "station:2", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 1 + + res3, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2469854, + Latitude: 37.8104049, + Name: "station:3", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 1 + // STEP_END + + // Output: + // 1 + // 1 + // 1 +} + +func ExampleClient_geosearch() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:rentable") + + _, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.27652, + Latitude: 37.805186, + Name: "station:1", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2674626, + Latitude: 37.8062344, + Name: "station:2", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2469854, + Latitude: 37.8104049, + Name: "station:3", + }).Result() + + if err != nil { + panic(err) + } + // REMOVE_END + + // STEP_START geosearch + res4, err := rdb.GeoSearch(ctx, "bikes:rentable", + &redis.GeoSearchQuery{ + Longitude: -122.27652, + Latitude: 37.805186, + Radius: 5, + RadiusUnit: "km", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [station:1 station:2 station:3] + // STEP_END + + // Output: + // [station:1 station:2 station:3] +} From cecf0307ac745d54efcc847fb3a777691d595612 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:38:32 +0100 Subject: [PATCH 055/230] DOC-4322 added HSET/HGET command page examples (#3140) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_hash_test.go | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 doctests/cmds_hash_test.go diff --git a/doctests/cmds_hash_test.go b/doctests/cmds_hash_test.go new file mode 100644 index 0000000000..f9630a9dee --- /dev/null +++ b/doctests/cmds_hash_test.go @@ -0,0 +1,133 @@ +// EXAMPLE: cmds_hash +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_hset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hset + res1, err := rdb.HSet(ctx, "myhash", "field1", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.HGet(ctx, "myhash", "field1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> Hello + + res3, err := rdb.HSet(ctx, "myhash", + "field2", "Hi", + "field3", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 2 + + res4, err := rdb.HGet(ctx, "myhash", "field2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> Hi + + res5, err := rdb.HGet(ctx, "myhash", "field3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> World + + res6, err := rdb.HGetAll(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) + // >>> map[field1:Hello field2:Hi field3:World] + // STEP_END + + // Output: + // 1 + // Hello + // 2 + // Hi + // World + // map[field1:Hello field2:Hi field3:World] +} + +func ExampleClient_hget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hget + res7, err := rdb.HSet(ctx, "myhash", "field1", "foo").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 1 + + res8, err := rdb.HGet(ctx, "myhash", "field1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> foo + + res9, err := rdb.HGet(ctx, "myhash", "field2").Result() + + if err != nil { + fmt.Println(err) + } + + fmt.Println(res9) // >>> + // STEP_END + + // Output: + // 1 + // foo + // redis: nil +} From 7fbbb5b15e829d681e4ea1ceca79b844053e8710 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:40:21 +0100 Subject: [PATCH 056/230] DOC-4323 added INCR command example (#3141) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_string_test.go | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 doctests/cmds_string_test.go diff --git a/doctests/cmds_string_test.go b/doctests/cmds_string_test.go new file mode 100644 index 0000000000..fb7801a673 --- /dev/null +++ b/doctests/cmds_string_test.go @@ -0,0 +1,57 @@ +// EXAMPLE: cmds_string +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cmd_incr() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START incr + incrResult1, err := rdb.Set(ctx, "mykey", "10", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult1) // >>> OK + + incrResult2, err := rdb.Incr(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult2) // >>> 11 + + incrResult3, err := rdb.Get(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult3) // >>> 11 + // STEP_END + + // Output: + // OK + // 11 + // 11 +} From 891bb2a7c8238621c8982f7b76f652d287eec7ab Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:42:24 +0100 Subject: [PATCH 057/230] DOC-4324 added ZADD and ZRANGE command examples (#3142) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_sorted_set_test.go | 220 +++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 doctests/cmds_sorted_set_test.go diff --git a/doctests/cmds_sorted_set_test.go b/doctests/cmds_sorted_set_test.go new file mode 100644 index 0000000000..8704fc20d5 --- /dev/null +++ b/doctests/cmds_sorted_set_test.go @@ -0,0 +1,220 @@ +// EXAMPLE: cmds_sorted_set +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_zadd_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zadd + zAddResult1, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult1) // >>> 1 + + zAddResult2, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "uno", Score: 1}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult2) + + zAddResult3, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult3) // >>> 2 + + zAddResult4, err := rdb.ZRangeWithScores(ctx, "myzset", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult4) // >>> [{1 one} {1 uno} {2 two} {3 three}] + // STEP_END + + // Output: + // 1 + // 1 + // 2 + // [{1 one} {1 uno} {2 two} {3 three}] +} + +func ExampleClient_zrange1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange1 + zrangeResult1, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult1) // >>> 3 + + zrangeResult2, err := rdb.ZRange(ctx, "myzset", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult2) // >>> [one two three] + + zrangeResult3, err := rdb.ZRange(ctx, "myzset", 2, 3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult3) // >>> [three] + + zrangeResult4, err := rdb.ZRange(ctx, "myzset", -2, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult4) // >>> [two three] + // STEP_END + + // Output: + // 3 + // [one two three] + // [three] + // [two three] +} + +func ExampleClient_zrange2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange2 + zRangeResult5, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult5) // >>> 3 + + zRangeResult6, err := rdb.ZRangeWithScores(ctx, "myzset", 0, 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult6) // >>> [{1 one} {2 two}] + // STEP_END + + // Output: + // 3 + // [{1 one} {2 two}] +} + +func ExampleClient_zrange3() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange3 + zRangeResult7, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult7) // >>> 3 + + zRangeResult8, err := rdb.ZRangeArgs(ctx, + redis.ZRangeArgs{ + Key: "myzset", + ByScore: true, + Start: "(1", + Stop: "+inf", + Offset: 1, + Count: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult8) // >>> [three] + // STEP_END + + // Output: + // 3 + // [three] +} From 76417485e2b2fd7cacf5cbb564c8b842fcffe7e1 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:44:29 +0100 Subject: [PATCH 058/230] DOC-4328 added DEL, EXPIRE, and TTL command examples (#3143) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_generic_test.go | 194 ++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 doctests/cmds_generic_test.go diff --git a/doctests/cmds_generic_test.go b/doctests/cmds_generic_test.go new file mode 100644 index 0000000000..ab8ebdd53f --- /dev/null +++ b/doctests/cmds_generic_test.go @@ -0,0 +1,194 @@ +// EXAMPLE: cmds_generic +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_del_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "key1", "key2", "key3") + // REMOVE_END + + // STEP_START del + delResult1, err := rdb.Set(ctx, "key1", "Hello", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult1) // >>> OK + + delResult2, err := rdb.Set(ctx, "key2", "World", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult2) // >>> OK + + delResult3, err := rdb.Del(ctx, "key1", "key2", "key3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult3) // >>> 2 + // STEP_END + + // Output: + // OK + // OK + // 2 +} + +func ExampleClient_expire_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START expire + expireResult1, err := rdb.Set(ctx, "mykey", "Hello", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult1) // >>> OK + + expireResult2, err := rdb.Expire(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult2) // >>> true + + expireResult3, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(expireResult3.Seconds())) // >>> 10 + + expireResult4, err := rdb.Set(ctx, "mykey", "Hello World", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult4) // >>> OK + + expireResult5, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult5) // >>> -1ns + + expireResult6, err := rdb.ExpireXX(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult6) // >>> false + + expireResult7, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult7) // >>> -1ns + + expireResult8, err := rdb.ExpireNX(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult8) // >>> true + + expireResult9, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(expireResult9.Seconds())) // >>> 10 + // STEP_END + + // Output: + // OK + // true + // 10 + // OK + // -1ns + // false + // -1ns + // true + // 10 +} + +func ExampleClient_ttl_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START ttl + ttlResult1, err := rdb.Set(ctx, "mykey", "Hello", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(ttlResult1) // >>> OK + + ttlResult2, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(ttlResult2.Seconds())) // >>> 10 + // STEP_END + + // Output: + // OK + // 10 +} From a7a0af2372f1b0d766830c38ac89018f4eb42b80 Mon Sep 17 00:00:00 2001 From: ofekshenawa Date: Mon, 11 Aug 2025 01:42:38 +0300 Subject: [PATCH 059/230] fix conflicts --- json.go | 8 +- json_test.go | 1417 ++++++++++++++++++++++++++++---------------------- 2 files changed, 793 insertions(+), 632 deletions(-) diff --git a/json.go b/json.go index 94254d914e..5dd34d8c50 100644 --- a/json.go +++ b/json.go @@ -60,7 +60,7 @@ type JSONArrTrimArgs struct { type JSONCmd struct { baseCmd val string - expanded []interface{} + expanded interface{} } var _ Cmder = (*JSONCmd)(nil) @@ -102,11 +102,11 @@ func (cmd *JSONCmd) Result() (string, error) { } // Expanded returns the result of the JSON.GET command as unmarshalled JSON. -func (cmd JSONCmd) Expanded() (interface{}, error) { +func (cmd *JSONCmd) Expanded() (interface{}, error) { if len(cmd.val) != 0 && cmd.expanded == nil { err := json.Unmarshal([]byte(cmd.val), &cmd.expanded) if err != nil { - return "", err + return nil, err } } @@ -500,7 +500,7 @@ func (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd } // JSONNumIncrBy increments the number value stored at the specified path by the provided number. -// For more information, see https://redis.io/commands/json.numincreby +// For more information, see https://redis.io/docs/latest/commands/json.numincrby/ func (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd { args := []interface{}{"JSON.NUMINCRBY", key, path, value} cmd := newJSONCmd(ctx, args...) diff --git a/json_test.go b/json_test.go index 2f559b992b..f8385b205f 100644 --- a/json_test.go +++ b/json_test.go @@ -2,6 +2,8 @@ package redis_test import ( "context" + "encoding/json" + "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" @@ -17,649 +19,808 @@ var _ = Describe("JSON Commands", Label("json"), func() { ctx := context.TODO() var client *redis.Client - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - Describe("arrays", Label("arrays"), func() { - It("should JSONArrAppend", Label("json.arrappend", "json"), func() { - cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{2, 3})) - }) - - It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() { - cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]int64{1})) - - cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal("OK")) - - res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(1))) - - res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(-1))) - - res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - stop := 5000 - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - stop = -1 - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(-1))) - }) - - It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() { - doc := `{ - "store": { - "book": [ - { - "category": "reference", - "author": "Nigel Rees", - "title": "Sayings of the Century", - "price": 8.95, - "size": [10, 20, 30, 40] - }, - { - "category": "fiction", - "author": "Evelyn Waugh", - "title": "Sword of Honour", - "price": 12.99, - "size": [50, 60, 70, 80] - }, - { - "category": "fiction", - "author": "Herman Melville", - "title": "Moby Dick", - "isbn": "0-553-21311-3", - "price": 8.99, - "size": [5, 10, 20, 30] - }, - { - "category": "fiction", - "author": "J. R. R. Tolkien", - "title": "The Lord of the Rings", - "isbn": "0-395-19395-8", - "price": 22.99, - "size": [5, 6, 7, 8] - } - ], - "bicycle": {"color": "red", "price": 19.95} - } - }` - res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]")) - - _, err = client.JSONGet(ctx, "this-key-does-not-exist", "$").Result() - Expect(err).To(HaveOccurred()) - Expect(err).To(BeIdenticalTo(redis.Nil)) - - resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resArr).To(Equal([]int64{1, 2})) - - }) - - It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { - cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{6})) - - cmd3 := client.JSONGet(ctx, "insert2") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - // RESP2 vs RESP3 - Expect(cmd3.Val()).To(Or( - Equal(`[100,200,300,1,2,200]`), - Equal(`[[100,200,300,1,2,200]]`))) - }) - - It("should JSONArrLen", Label("json.arrlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrLen(ctx, "length2", "$..a") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{1, 6})) + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, }) + } - It("should JSONArrPop", Label("json.arrpop"), func() { - cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]string{"300"})) - - cmd3 := client.JSONGet(ctx, "pop4", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) - }) - - It("should JSONArrTrim", Label("json.arrtrim", "json"), func() { - cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - stop := 3 - cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]int64{3})) - - res, err := client.JSONGet(ctx, "trim1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[[1,2,3]]`)) - - cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal("OK")) - - stop = 3 - cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd4).To(Equal([]int64{0})) - - cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd5).To(Equal("OK")) - - stop = 99 - cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd6).To(Equal([]int64{2})) - - cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd7).To(Equal("OK")) - - stop = 1 - cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd8).To(Equal([]int64{0})) - - cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd9).To(Equal("OK")) - - stop = 11 - cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd10).To(Equal([]int64{0})) - }) - - It("should JSONArrPop", Label("json.arrpop", "json"), func() { - cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]string{"300"})) - - cmd3 := client.JSONGet(ctx, "pop4", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) - }) + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } }) - Describe("get/set", Label("getset"), func() { - It("should JSONSet", Label("json.set", "json"), func() { - cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`) - Expect(cmd.Err()).NotTo(HaveOccurred()) - Expect(cmd.Val()).To(Equal("OK")) + protocols := []int{2, 3} + for _, protocol := range protocols { + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) }) - It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() { - res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`{-"a":1,-"b":2}`)) - - res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`)) + Describe("arrays", Label("arrays"), func() { + It("should JSONArrAppend", Label("json.arrappend", "json"), func() { + cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{2, 3})) + }) + + It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() { + cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]int64{1})) + + cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal("OK")) + + res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(1))) + + res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(-1))) + + res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + stop := 5000 + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + stop = -1 + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(-1))) + }) + + It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() { + doc := `{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "size": [10, 20, 30, 40] + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + "size": [50, 60, 70, 80] + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "size": [5, 10, 20, 30] + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "size": [5, 6, 7, 8] + } + ], + "bicycle": {"color": "red", "price": 19.95} + } + }` + res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]")) + + resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resArr).To(Equal([]int64{1, 2})) + }) + + It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { + cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{6})) + + cmd3 := client.JSONGet(ctx, "insert2") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + // RESP2 vs RESP3 + Expect(cmd3.Val()).To(Or( + Equal(`[100,200,300,1,2,200]`), + Equal(`[[100,200,300,1,2,200]]`))) + }) + + It("should JSONArrLen", Label("json.arrlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrLen(ctx, "length2", "$..a") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{1, 6})) + }) + + It("should JSONArrPop", Label("json.arrpop"), func() { + cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]string{"300"})) + + cmd3 := client.JSONGet(ctx, "pop4", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) + }) + + It("should JSONArrTrim", Label("json.arrtrim", "json"), func() { + cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + stop := 3 + cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]int64{3})) + + res, err := client.JSONGet(ctx, "trim1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[[1,2,3]]`)) + + cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal("OK")) + + stop = 3 + cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd4).To(Equal([]int64{0})) + + cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd5).To(Equal("OK")) + + stop = 99 + cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd6).To(Equal([]int64{2})) + + cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd7).To(Equal("OK")) + + stop = 1 + cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd8).To(Equal([]int64{0})) + + cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd9).To(Equal("OK")) + + stop = 11 + cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd10).To(Equal([]int64{0})) + }) + + It("should JSONArrPop", Label("json.arrpop", "json"), func() { + cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]string{"300"})) + + cmd3 := client.JSONGet(ctx, "pop4", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) + }) }) - It("should JSONMerge", Label("json.merge", "json"), func() { - res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONGet(ctx, "merge1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`)) + Describe("get/set", Label("getset"), func() { + It("should JSONSet", Label("json.set", "json"), func() { + cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`) + Expect(cmd.Err()).NotTo(HaveOccurred()) + Expect(cmd.Val()).To(Equal("OK")) + }) + + It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() { + res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + _, err = client.JSONGet(ctx, "this-key-does-not-exist", "$").Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(BeIdenticalTo(redis.Nil)) + + resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resArr).To(Equal([]int64{1, 2})) + + res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`{-"a":1,-"b":2}`)) + + res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`)) + }) + + It("should JSONMerge", Label("json.merge", "json"), func() { + res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONGet(ctx, "merge1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`)) + }) + + It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() { + doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`} + doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2} + docs := []redis.JSONSetArgs{doc1, doc2} + + mSetResult, err := client.JSONMSetArgs(ctx, docs).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(mSetResult).To(Equal("OK")) + + res, err := client.JSONMGet(ctx, "$", "mset1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]interface{}{`[{"a":1}]`})) + + res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"})) + + _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() { + cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal("OK")) + + cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(HaveLen(2)) + Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`)) + Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`)) + }) + + It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{`[1,3,""]`})) + + iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`})) + + iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{nil, nil})) + }) }) - It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() { - doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`} - doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2} - docs := []redis.JSONSetArgs{doc1, doc2} - - mSetResult, err := client.JSONMSetArgs(ctx, docs).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(mSetResult).To(Equal("OK")) - - res, err := client.JSONMGet(ctx, "$", "mset1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal([]interface{}{`[{"a":1}]`})) - - res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"})) - - _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result() - Expect(err).NotTo(HaveOccurred()) + Describe("Misc", Label("misc"), func() { + It("should JSONClear", Label("json.clear", "json"), func() { + cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONClear(ctx, "clear1", "$") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(1))) + + cmd3 := client.JSONGet(ctx, "clear1", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal(`[[]]`)) + }) + + It("should JSONClear with $", Label("json.clear", "json"), func() { + doc := `{ + "nested1": {"a": {"foo": 10, "bar": 20}}, + "a": ["foo"], + "nested2": {"a": "claro"}, + "nested3": {"a": {"baz": 50}} + }` + res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resGet, err := client.JSONGet(ctx, "doc1", `$`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`)) + + res, err = client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "doc1", `$`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`)) + }) + + It("should JSONDel", Label("json.del", "json"), func() { + cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONDel(ctx, "del1", "$") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(1))) + + cmd3 := client.JSONGet(ctx, "del1", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(HaveLen(0)) + }) + + It("should JSONDel with $", Label("json.del", "json"), func() { + res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONDel(ctx, "del1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(2))) + + resGet, err := client.JSONGet(ctx, "del1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) + + res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONDel(ctx, "del2", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "del2", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) + + doc := `[ + { + "ciao": ["non ancora"], + "nested": [ + {"ciao": [1, "a"]}, + {"ciao": [2, "a"]}, + {"ciaoc": [3, "non", "ciao"]}, + {"ciao": [4, "a"]}, + {"e": [5, "non", "ciao"]} + ] + } + ]` + res, err = client.JSONSet(ctx, "del3", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` + resGet, err = client.JSONGet(ctx, "del3", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(resVal)) + }) + + It("should JSONForget", Label("json.forget", "json"), func() { + cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONForget(ctx, "forget3", "$..a") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(2))) + + cmd3 := client.JSONGet(ctx, "forget3", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`)) + }) + + It("should JSONForget with $", Label("json.forget", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(2))) + + resGet, err := client.JSONGet(ctx, "doc1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "doc2", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) + + doc := `[ + { + "ciao": ["non ancora"], + "nested": [ + {"ciao": [1, "a"]}, + {"ciao": [2, "a"]}, + {"ciaoc": [3, "non", "ciao"]}, + {"ciao": [4, "a"]}, + {"e": [5, "non", "ciao"]} + ] + } + ]` + res, err = client.JSONSet(ctx, "doc3", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` + resGet, err = client.JSONGet(ctx, "doc3", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(resVal)) + }) + + It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() { + cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1)) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(`[3,0]`)) + }) + + It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[7]`)) + + res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[10.5]`)) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[5]`)) + }) + + It("should JSONObjKeys", Label("json.objkeys", "json"), func() { + cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(7)) + Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil})) + }) + + It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() { + doc := `{ + "nested1": {"a": {"foo": 10, "bar": 20}}, + "a": ["foo"], + "nested2": {"a": {"baz": 50}} + }` + cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}})) + + cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{"foo", "bar"})) + + cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{"baz"})) + + _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should JSONObjLen", Label("json.objlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(7)) + Expect(cmd2.Val()[0]).To(BeNil()) + Expect(*cmd2.Val()[1]).To(Equal(int64(1))) + }) + + It("should JSONStrLen", Label("json.strlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(5)) + var tmp int64 = 20 + Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp)) + Expect(*cmd2.Val()[0]).To(Equal(int64(5))) + Expect(*cmd2.Val()[1]).To(Equal(int64(3))) + Expect(cmd2.Val()[2]).To(BeNil()) + Expect(*cmd2.Val()[3]).To(Equal(int64(5))) + Expect(*cmd2.Val()[4]).To(Equal(int64(3))) + }) + + It("should JSONStrAppend", Label("json.strappend", "json"), func() { + cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*cmd2[0]).To(Equal(int64(6))) + cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal(`["foobar"]`)) + }) + + It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*intArrayResult[0]).To(Equal(int64(8))) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*intResult[0]).To(Equal(int64(5))) + }) + + It("should JSONToggle", Label("json.toggle", "json"), func() { + cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(1)) + Expect(*cmd2.Val()[0]).To(Equal(int64(0))) + }) + + It("should JSONType", Label("json.type", "json"), func() { + cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONType(ctx, "type1", "$[0]") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(1)) + // RESP2 v RESP3 + Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) + }) }) + } +}) - It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() { - cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal("OK")) - - cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(HaveLen(2)) - Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`)) - Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`)) +var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() { + var client *redis.Client + var ctx = context.Background() + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, // Setting RESP2 or RESP3 protocol + UnstableResp3: true, // Enable RESP3 features }) + } - It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{`[1,3,""]`})) - - iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`})) - - iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{nil, nil})) - }) + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } }) - Describe("Misc", Label("misc"), func() { - It("should JSONClear", Label("json.clear", "json"), func() { - cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONClear(ctx, "clear1", "$") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(1))) - - cmd3 := client.JSONGet(ctx, "clear1", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal(`[[]]`)) - }) - - It("should JSONClear with $", Label("json.clear", "json"), func() { - doc := `{ - "nested1": {"a": {"foo": 10, "bar": 20}}, - "a": ["foo"], - "nested2": {"a": "claro"}, - "nested3": {"a": {"baz": 50}} - }` - res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resGet, err := client.JSONGet(ctx, "doc1", `$`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`)) - - res, err = client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "doc1", `$`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`)) - }) - - It("should JSONDel", Label("json.del", "json"), func() { - cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONDel(ctx, "del1", "$") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(1))) - - cmd3 := client.JSONGet(ctx, "del1", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(HaveLen(0)) - }) - - It("should JSONDel with $", Label("json.del", "json"), func() { - res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONDel(ctx, "del1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(2))) - - resGet, err := client.JSONGet(ctx, "del1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) - - res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONDel(ctx, "del2", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "del2", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) - - doc := `[ - { - "ciao": ["non ancora"], - "nested": [ - {"ciao": [1, "a"]}, - {"ciao": [2, "a"]}, - {"ciaoc": [3, "non", "ciao"]}, - {"ciao": [4, "a"]}, - {"e": [5, "non", "ciao"]} - ] - } - ]` - res, err = client.JSONSet(ctx, "del3", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` - resGet, err = client.JSONGet(ctx, "del3", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(resVal)) - }) - - It("should JSONForget", Label("json.forget", "json"), func() { - cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONForget(ctx, "forget3", "$..a") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(2))) - - cmd3 := client.JSONGet(ctx, "forget3", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`)) - }) - - It("should JSONForget with $", Label("json.forget", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(2))) - - resGet, err := client.JSONGet(ctx, "doc1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "doc2", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) - - doc := `[ - { - "ciao": ["non ancora"], - "nested": [ - {"ciao": [1, "a"]}, - {"ciao": [2, "a"]}, - {"ciaoc": [3, "non", "ciao"]}, - {"ciao": [4, "a"]}, - {"e": [5, "non", "ciao"]} - ] - } - ]` - res, err = client.JSONSet(ctx, "doc3", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` - resGet, err = client.JSONGet(ctx, "doc3", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(resVal)) - }) - - It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() { - cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1)) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(`[3,0]`)) - }) - - It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[7]`)) - - res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[10.5]`)) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[5]`)) - }) - - It("should JSONObjKeys", Label("json.objkeys", "json"), func() { - cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(7)) - Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil})) - }) - - It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() { - doc := `{ - "nested1": {"a": {"foo": 10, "bar": 20}}, - "a": ["foo"], - "nested2": {"a": {"baz": 50}} - }` - cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}})) - - cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{"foo", "bar"})) - - cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{"baz"})) - - _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result() - Expect(err).To(HaveOccurred()) - }) - - It("should JSONObjLen", Label("json.objlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(7)) - Expect(cmd2.Val()[0]).To(BeNil()) - Expect(*cmd2.Val()[1]).To(Equal(int64(1))) - }) - - It("should JSONStrLen", Label("json.strlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(5)) - var tmp int64 = 20 - Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp)) - Expect(*cmd2.Val()[0]).To(Equal(int64(5))) - Expect(*cmd2.Val()[1]).To(Equal(int64(3))) - Expect(cmd2.Val()[2]).To(BeNil()) - Expect(*cmd2.Val()[3]).To(Equal(int64(5))) - Expect(*cmd2.Val()[4]).To(Equal(int64(3))) - }) - - It("should JSONStrAppend", Label("json.strappend", "json"), func() { - cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*cmd2[0]).To(Equal(int64(6))) - cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal(`["foobar"]`)) - }) - - It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*intArrayResult[0]).To(Equal(int64(8))) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*intResult[0]).To(Equal(int64(5))) - }) - - It("should JSONToggle", Label("json.toggle", "json"), func() { - cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(1)) - Expect(*cmd2.Val()[0]).To(Equal(int64(0))) - }) - - It("should JSONType", Label("json.type", "json"), func() { - cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONType(ctx, "type1", "$[0]") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(1)) - // RESP2 v RESP3 - Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) - }) + Context("when testing with RESP2 and RESP3", func() { + protocols := []int{2, 3} + + for _, protocol := range protocols { + When("using protocol version", func() { + BeforeEach(func() { + client = setupRedisClient(protocol) + }) + + It("should perform complex JSON and RediSearch operations", func() { + jsonDoc := map[string]interface{}{ + "person": map[string]interface{}{ + "name": "Alice", + "age": 30, + "status": true, + "address": map[string]interface{}{ + "city": "Wonderland", + "postcode": "12345", + }, + "contacts": []map[string]interface{}{ + {"type": "email", "value": "alice@example.com"}, + {"type": "phone", "value": "+123456789"}, + {"type": "fax", "value": "+987654321"}, + }, + "friends": []map[string]interface{}{ + {"name": "Bob", "age": 35, "status": true}, + {"name": "Charlie", "age": 28, "status": false}, + }, + }, + "settings": map[string]interface{}{ + "notifications": map[string]interface{}{ + "email": true, + "sms": false, + "alerts": []string{"low battery", "door open"}, + }, + "theme": "dark", + }, + } + + setCmd := client.JSONSet(ctx, "person:1", ".", jsonDoc) + Expect(setCmd.Err()).NotTo(HaveOccurred(), "JSON.SET failed") + + getCmdRaw := client.JSONGet(ctx, "person:1", ".") + rawJSON, err := getCmdRaw.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.GET (raw) failed") + GinkgoWriter.Printf("Raw JSON: %s\n", rawJSON) + + getCmdExpanded := client.JSONGet(ctx, "person:1", ".") + expandedJSON, err := getCmdExpanded.Expanded() + Expect(err).NotTo(HaveOccurred(), "JSON.GET (expanded) failed") + GinkgoWriter.Printf("Expanded JSON: %+v\n", expandedJSON) + + Expect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON))) + + arrAppendCmd := client.JSONArrAppend(ctx, "person:1", "$.person.contacts", `{"type": "social", "value": "@alice_wonder"}`) + Expect(arrAppendCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRAPPEND failed") + arrLenCmd := client.JSONArrLen(ctx, "person:1", "$.person.contacts") + arrLen, err := arrLenCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.ARRLEN failed") + Expect(arrLen).To(Equal([]int64{4}), "Array length mismatch after append") + + arrInsertCmd := client.JSONArrInsert(ctx, "person:1", "$.person.friends", 1, `{"name": "Diana", "age": 25, "status": true}`) + Expect(arrInsertCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRINSERT failed") + + start := 0 + stop := 1 + arrTrimCmd := client.JSONArrTrimWithArgs(ctx, "person:1", "$.person.friends", &redis.JSONArrTrimArgs{Start: start, Stop: &stop}) + Expect(arrTrimCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRTRIM failed") + + mergeData := map[string]interface{}{ + "status": false, + "nickname": "WonderAlice", + "lastLogin": time.Now().Format(time.RFC3339), + } + mergeCmd := client.JSONMerge(ctx, "person:1", "$.person", jsonMustMarshal(mergeData)) + Expect(mergeCmd.Err()).NotTo(HaveOccurred(), "JSON.MERGE failed") + + typeCmd := client.JSONType(ctx, "person:1", "$.person.nickname") + nicknameType, err := typeCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.TYPE failed") + Expect(nicknameType[0]).To(Equal([]interface{}{"string"}), "JSON.TYPE mismatch for nickname") + + createIndexCmd := client.Do(ctx, "FT.CREATE", "person_idx", "ON", "JSON", + "PREFIX", "1", "person:", "SCHEMA", + "$.person.name", "AS", "name", "TEXT", + "$.person.age", "AS", "age", "NUMERIC", + "$.person.address.city", "AS", "city", "TEXT", + "$.person.contacts[*].value", "AS", "contact_value", "TEXT", + ) + Expect(createIndexCmd.Err()).NotTo(HaveOccurred(), "FT.CREATE failed") + + searchCmd := client.FTSearchWithArgs(ctx, "person_idx", "@contact_value:(alice\\@example\\.com alice_wonder)", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.person.name"}, {FieldName: "$.person.age"}, {FieldName: "$.person.address.city"}}}) + searchResult, err := searchCmd.Result() + Expect(err).NotTo(HaveOccurred(), "FT.SEARCH failed") + GinkgoWriter.Printf("Advanced Search result: %+v\n", searchResult) + + incrCmd := client.JSONNumIncrBy(ctx, "person:1", "$.person.age", 5) + incrResult, err := incrCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.NUMINCRBY failed") + Expect(incrResult).To(Equal("[35]"), "Age increment mismatch") + + delCmd := client.JSONDel(ctx, "person:1", "$.settings.notifications.email") + Expect(delCmd.Err()).NotTo(HaveOccurred(), "JSON.DEL failed") + + typeCmd = client.JSONType(ctx, "person:1", "$.settings.notifications.email") + typeResult, err := typeCmd.Result() + Expect(err).ToNot(HaveOccurred()) + Expect(typeResult[0]).To(BeEmpty(), "Expected JSON.TYPE to be empty for deleted field") + }) + }) + } }) }) + +// Helper function to marshal data into JSON for comparisons +func jsonMustMarshal(v interface{}) string { + bytes, err := json.Marshal(v) + Expect(err).NotTo(HaveOccurred()) + return string(bytes) +} From d592eedde62a1ff28151cd39cb4887e08edafb51 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:52:50 +0100 Subject: [PATCH 060/230] Fix field name spellings (#3132) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- search_commands.go | 24 ++++++++++++------------ search_test.go | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/search_commands.go b/search_commands.go index 1a8a4cfef4..e4df0b6fc5 100644 --- a/search_commands.go +++ b/search_commands.go @@ -16,7 +16,7 @@ type SearchCmdable interface { FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd FTAliasDel(ctx context.Context, alias string) *StatusCmd FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd - FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd + FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd @@ -57,7 +57,7 @@ type FTCreateOptions struct { NoFields bool NoFreqs bool StopWords []interface{} - SkipInitalScan bool + SkipInitialScan bool } type FieldSchema struct { @@ -70,7 +70,7 @@ type FieldSchema struct { NoIndex bool PhoneticMatcher string Weight float64 - Seperator string + Separator string CaseSensitive bool WithSuffixtrie bool VectorArgs *FTVectorArgs @@ -285,7 +285,7 @@ type FTSearchSortBy struct { type FTSearchOptions struct { NoContent bool Verbatim bool - NoStopWrods bool + NoStopWords bool WithScores bool WithPayloads bool WithSortKeys bool @@ -808,13 +808,13 @@ func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) } // FTAlter - Alters the definition of an existing index. -// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan. +// The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan. // The 'definition' parameter specifies the new definition for the index. // For more information, please refer to the Redis documentation: // [FT.ALTER]: (https://redis.io/commands/ft.alter/) -func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd { +func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd { args := []interface{}{"FT.ALTER", index} - if skipInitalScan { + if skipInitialScan { args = append(args, "SKIPINITIALSCAN") } args = append(args, "SCHEMA", "ADD") @@ -907,7 +907,7 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, "STOPWORDS", len(options.StopWords)) args = append(args, options.StopWords...) } - if options.SkipInitalScan { + if options.SkipInitialScan { args = append(args, "SKIPINITIALSCAN") } } @@ -1003,8 +1003,8 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.Weight > 0 { args = append(args, "WEIGHT", schema.Weight) } - if schema.Seperator != "" { - args = append(args, "SEPERATOR", schema.Seperator) + if schema.Separator != "" { + args = append(args, "SEPARATOR", schema.Separator) } if schema.CaseSensitive { args = append(args, "CASESENSITIVE") @@ -1694,7 +1694,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } - if options.NoStopWrods { + if options.NoStopWords { queryArgs = append(queryArgs, "NOSTOPWORDS") } if options.WithScores { @@ -1808,7 +1808,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin if options.Verbatim { args = append(args, "VERBATIM") } - if options.NoStopWrods { + if options.NoStopWords { args = append(args, "NOSTOPWORDS") } if options.WithScores { diff --git a/search_test.go b/search_test.go index efdc6bb1e5..48b9aa39bc 100644 --- a/search_test.go +++ b/search_test.go @@ -637,11 +637,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) - It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() { + It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() { client.HSet(ctx, "doc1", "foo", "bar") text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result() + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "idx1") From 53102b69cdb4b37daa1629c84a1e52b02521bf3c Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:32:53 +0300 Subject: [PATCH 061/230] Updated package version (#3158) --- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 715454c657..d725db0bbb 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index f68ff25d55..7093be4202 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 176e03d095..85a82860a5 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 2b5030ab63..4d97da4d1e 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index d8e9bfffe0..3825432a7d 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 45423ec525..fca1a59720 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 33221d208c..bae3f7b939 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 7bc65f9ed0..594cfdf1e1 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 3a95b56e95..b2e30b3947 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 342836007b..5cbafac11a 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/version.go b/version.go index b1234dac3a..7cb060b5d7 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.6.1" + return "9.6.2" } From d0bd7390f28a5ed2fe0b84639771e19dbc96c0d9 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:47:25 +0100 Subject: [PATCH 062/230] DOC-4232 stream code examples (#3128) * DOC-4232 added first stream example * DOC-4232 examples up to xadd_7 * DOC-4232 examples up to xread * DOC-4232 examples up to xclaim * DOC-4232 added remaining examples * DOC-4232 more fixes * DOC-4232 fix for test fail on CI build --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/stream_tutorial_test.go | 1073 ++++++++++++++++++++++++++++++ 1 file changed, 1073 insertions(+) create mode 100644 doctests/stream_tutorial_test.go diff --git a/doctests/stream_tutorial_test.go b/doctests/stream_tutorial_test.go new file mode 100644 index 0000000000..0933247056 --- /dev/null +++ b/doctests/stream_tutorial_test.go @@ -0,0 +1,1073 @@ +// EXAMPLE: stream_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +// REMOVE_START +func UNUSED(v ...interface{}) {} + +// REMOVE_END + +func ExampleClient_xadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + // STEP_START xadd + res1, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res1) // >>> 1692632086370-0 + + res2, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.PrintLn(res2) // >>> 1692632094485-0 + + res3, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res3) // >>> 1692632102976-0 + // STEP_END + + // REMOVE_START + UNUSED(res1, res2, res3) + // REMOVE_END + + xlen, err := rdb.XLen(ctx, "race:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(xlen) // >>> 3 + + // Output: + // 3 +} + +func ExampleClient_racefrance1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + ID: "1692632094485-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + ID: "1692632102976-0", + }).Result() + + if err != nil { + panic(err) + } + + // STEP_START xrange + res4, err := rdb.XRangeN(ctx, "race:france", "1691765278160-0", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... + // STEP_END + + // STEP_START xread_block + res5, err := rdb.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"race:france", "0"}, + Count: 100, + Block: 300, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> // [{race:france [{1692632086370-0 map[location_id:1 position:1... + // STEP_END + + // STEP_START xadd_2 + res6, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 29.9, + "position": 1, + "location_id": 2, + }, + }).Result() + + if err != nil { + panic(err) + } + + //fmt.Println(res6) // >>> 1692632147973-0 + // STEP_END + + // STEP_START xlen + res7, err := rdb.XLen(ctx, "race:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 4 + // STEP_END + + // REMOVE_START + UNUSED(res6) + // REMOVE_END + + // Output: + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]}]}] + // 4 +} + +func ExampleClient_raceusa() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:usa") + // REMOVE_END + + // STEP_START xadd_id + res8, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Castilla", + }, + ID: "0-1", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 0-1 + + res9, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Norem", + }, + ID: "0-2", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 0-2 + // STEP_END + + // STEP_START xadd_bad_id + res10, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Values: map[string]interface{}{ + "racer": "Prickett", + }, + ID: "0-1", + }).Result() + + if err != nil { + // fmt.Println(err) + // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item + } + // STEP_END + + // STEP_START xadd_7 + res11, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Prickett", + }, + ID: "0-*", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 0-3 + // STEP_END + + // REMOVE_START + UNUSED(res10) + // REMOVE_END + + // Output: + // 0-1 + // 0-2 + // 0-3 +} + +func ExampleClient_racefrance2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + ID: "1692632094485-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + ID: "1692632102976-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 29.9, + "position": 1, + "location_id": 2, + }, + ID: "1692632147973-0", + }).Result() + + if err != nil { + panic(err) + } + // STEP_START xrange_all + res12, err := rdb.XRange(ctx, "race:france", "-", "+").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... + // STEP_END + + // STEP_START xrange_time + res13, err := rdb.XRange(ctx, "race:france", + "1692632086369", "1692632086371", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]}] + // STEP_END + + // STEP_START xrange_step_1 + res14, err := rdb.XRangeN(ctx, "race:france", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // STEP_END + + // STEP_START xrange_step_2 + res15, err := rdb.XRangeN(ctx, "race:france", + "(1692632094485-0", "+", 2, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) + // >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // STEP_END + + // STEP_START xrange_empty + res16, err := rdb.XRangeN(ctx, "race:france", + "(1692632147973-0", "+", 2, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) + // >>> [] + // STEP_END + + // STEP_START xrevrange + res17, err := rdb.XRevRangeN(ctx, "race:france", "+", "-", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) + // >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // STEP_END + + // STEP_START xread + res18, err := rdb.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"race:france", "0"}, + Count: 2, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) + // >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]}] + // STEP_END + + // Output: + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]}] + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [] + // [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]}] +} + +func ExampleClient_xgroupcreate() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + // STEP_START xgroup_create + res19, err := rdb.XGroupCreate(ctx, "race:france", "france_riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_xgroupcreatemkstream() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + // STEP_START xgroup_create_mkstream + res20, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_xgroupread() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + _, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START xgroup_read + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Castilla"}, + }).Result() + // >>> 1692632639151-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Royce"}, + }).Result() + // >>> 1692632647899-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Sam-Bodden"}, + }).Result() + // >>> 1692632662819-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Prickett"}, + }).Result() + // >>> 1692632670501-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Norem"}, + }).Result() + // >>> 1692632678249-0 + + if err != nil { + panic(err) + } + + // fmt.Println(res25) + + res21, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Alice", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res21) + // >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // STEP_END + + // REMOVE_START + UNUSED(res21) + // REMOVE_END + + xlen, err := rdb.XLen(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(xlen) + + // Output: + // 5 +} + +func ExampleClient_raceitaly() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + rdb.XGroupDestroy(ctx, "race:italy", "italy_riders") + // REMOVE_END + + _, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Castilla"}, + ID: "1692632639151-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Royce"}, + ID: "1692632647899-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Sam-Bodden"}, + ID: "1692632662819-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Prickett"}, + ID: "1692632670501-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Norem"}, + ID: "1692632678249-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Alice", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + // STEP_START xgroup_read_id + res22, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", "0"}, + Group: "italy_riders", + Consumer: "Alice", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) + // >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // STEP_END + + // STEP_START xack + res23, err := rdb.XAck(ctx, + "race:italy", "italy_riders", "1692632639151-0", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> 1 + + res24, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", "0"}, + Group: "italy_riders", + Consumer: "Alice", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) + // >>> [{race:italy []}] + // STEP_END + + // STEP_START xgroup_read_bob + res25, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Bob", + Count: 2, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) + // >>> [{race:italy [{1692632647899-0 map[rider:Royce]} {1692632662819-0 map[rider:Sam-Bodden]}]}] + + // STEP_END + + // STEP_START xpending + res26, err := rdb.XPending(ctx, "race:italy", "italy_riders").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res26) + // >>> &{2 1692632647899-0 1692632662819-0 map[Bob:2]} + // STEP_END + + // STEP_START xpending_plus_minus + res27, err := rdb.XPendingExt(ctx, &redis.XPendingExtArgs{ + Stream: "race:italy", + Group: "italy_riders", + Start: "-", + End: "+", + Count: 10, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res27) + // >>> [{1692632647899-0 Bob 0s 1} {1692632662819-0 Bob 0s 1}] + // STEP_END + + // STEP_START xrange_pending + res28, err := rdb.XRange(ctx, "race:italy", + "1692632647899-0", "1692632647899-0", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce]}] + // STEP_END + + // STEP_START xclaim + res29, err := rdb.XClaim(ctx, &redis.XClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Alice", + MinIdle: 0, + Messages: []string{"1692632647899-0"}, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res29) + // STEP_END + + // STEP_START xautoclaim + res30, res30a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Alice", + Start: "0-0", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res30) // >>> [{1692632647899-0 map[rider:Royce]}] + fmt.Println(res30a) // >>> 1692632662819-0 + // STEP_END + + // STEP_START xautoclaim_cursor + res31, res31a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Lora", + Start: "(1692632662819-0", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res31) // >>> [] + fmt.Println(res31a) // >>> 0-0 + // STEP_END + + // STEP_START xinfo + res32, err := rdb.XInfoStream(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res32) + // >>> &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla]} {1692632678249-0 map[rider:Norem]} 1692632639151-0} + // STEP_END + + // STEP_START xinfo_groups + res33, err := rdb.XInfoGroups(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res33) + // >>> [{italy_riders 3 2 1692632662819-0 3 2}] + // STEP_END + + // STEP_START xinfo_consumers + res34, err := rdb.XInfoConsumers(ctx, "race:italy", "italy_riders").Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res34) + // >>> [{Alice 1 1ms 1ms} {Bob 1 2ms 2ms} {Lora 0 1ms -1ms}] + // STEP_END + + // STEP_START maxlen + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Jones"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Wood"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Henshaw"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + res35, err := rdb.XLen(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res35) // >>> 2 + + res36, err := rdb.XRange(ctx, "race:italy", "-", "+").Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res36) + // >>> [{1726649529170-1 map[rider:Wood]} {1726649529171-0 map[rider:Henshaw]}] + // STEP_END + + // STEP_START xtrim + res37, err := rdb.XTrimMaxLen(ctx, "race:italy", 10).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res37) // >>> 0 + // STEP_END + + // STEP_START xtrim2 + res38, err := rdb.XTrimMaxLenApprox(ctx, "race:italy", 10, 20).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res38) // >>> 0 + // STEP_END + + // REMOVE_START + UNUSED(res27, res34, res36) + // REMOVE_END + + // Output: + // [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // 1 + // [{race:italy []}] + // [{race:italy [{1692632647899-0 map[rider:Royce]} {1692632662819-0 map[rider:Sam-Bodden]}]}] + // &{2 1692632647899-0 1692632662819-0 map[Bob:2]} + // [{1692632647899-0 map[rider:Royce]}] + // [{1692632647899-0 map[rider:Royce]}] + // [{1692632647899-0 map[rider:Royce]}] + // 1692632662819-0 + // [] + // 0-0 + // &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla]} {1692632678249-0 map[rider:Norem]} 1692632639151-0} + // [{italy_riders 3 2 1692632662819-0 3 2}] + // 2 + // 0 + // 0 +} + +func ExampleClient_xdel() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Wood"}, + ID: "1692633198206-0", + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Henshaw"}, + ID: "1692633208557-0", + }, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START xdel + res39, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res39) + // >>> [{1692633198206-0 map[rider:Wood]} {1692633208557-0 map[rider:Henshaw]}] + + res40, err := rdb.XDel(ctx, "race:italy", "1692633208557-0").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res40) // 1 + + res41, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res41) + // >>> [{1692633198206-0 map[rider:Wood]}] + // STEP_END + + // Output: + // [{1692633198206-0 map[rider:Wood]} {1692633208557-0 map[rider:Henshaw]}] + // 1 + // [{1692633198206-0 map[rider:Wood]}] +} From 1260350692737bb0d9ae0e99fcd525254fd72e9b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:25:46 +0000 Subject: [PATCH 063/230] DOC-4345 added JSON samples for home page (#3183) --- doctests/home_json_example_test.go | 199 +++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 doctests/home_json_example_test.go diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go new file mode 100644 index 0000000000..b9e46a638a --- /dev/null +++ b/doctests/home_json_example_test.go @@ -0,0 +1,199 @@ +// EXAMPLE: go_home_json +// HIDE_START +package example_commands_test + +// HIDE_END +// STEP_START import +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +// STEP_END + +func ExampleClient_search_json() { + // STEP_START connect + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // STEP_END + // REMOVE_START + rdb.Del(ctx, "user:1", "user:2", "user:3") + rdb.FTDropIndex(ctx, "idx:users") + // REMOVE_END + + // STEP_START create_data + user1 := map[string]interface{}{ + "name": "Paul John", + "email": "paul.john@example.com", + "age": 42, + "city": "London", + } + + user2 := map[string]interface{}{ + "name": "Eden Zamir", + "email": "eden.zamir@example.com", + "age": 29, + "city": "Tel Aviv", + } + + user3 := map[string]interface{}{ + "name": "Paul Zamir", + "email": "paul.zamir@example.com", + "age": 35, + "city": "Tel Aviv", + } + // STEP_END + + // STEP_START make_index + _, err := rdb.FTCreate( + ctx, + "idx:users", + // Options: + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"user:"}, + }, + // Index schema fields: + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.city", + As: "city", + FieldType: redis.SearchFieldTypeTag, + }, + &redis.FieldSchema{ + FieldName: "$.age", + As: "age", + FieldType: redis.SearchFieldTypeNumeric, + }, + ).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START add_data + _, err = rdb.JSONSet(ctx, "user:1", "$", user1).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.JSONSet(ctx, "user:2", "$", user2).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.JSONSet(ctx, "user:3", "$", user3).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START query1 + findPaulResult, err := rdb.FTSearch( + ctx, + "idx:users", + "Paul @age:[30 40]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(findPaulResult) + // >>> {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv"... + // STEP_END + + // STEP_START query2 + citiesResult, err := rdb.FTSearchWithArgs( + ctx, + "idx:users", + "Paul", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "$.city", + As: "city", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + sort.Slice(citiesResult.Docs, func(i, j int) bool { + return citiesResult.Docs[i].Fields["city"] < citiesResult.Docs[j].Fields["city"] + }) + + for _, result := range citiesResult.Docs { + fmt.Println(result.Fields["city"]) + } + // >>> London + // >>> Tel Aviv + // STEP_END + + // STEP_START query3 + aggOptions := redis.FTAggregateOptions{ + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@city"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchCount, + As: "count", + }, + }, + }, + }, + } + + aggResult, err := rdb.FTAggregateWithArgs( + ctx, + "idx:users", + "*", + &aggOptions, + ).Result() + + if err != nil { + panic(err) + } + + sort.Slice(aggResult.Rows, func(i, j int) bool { + return aggResult.Rows[i].Fields["city"].(string) < + aggResult.Rows[j].Fields["city"].(string) + }) + + for _, row := range aggResult.Rows { + fmt.Printf("%v - %v\n", + row.Fields["city"], row.Fields["count"], + ) + } + // >>> City: London - 1 + // >>> City: Tel Aviv - 2 + // STEP_END + + // Output: + // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} + // London + // Tel Aviv + // London - 1 + // Tel Aviv - 2 +} From 73b4f38fb4795b0caf00da337db3fee4dd7fc08f Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:27:00 +0200 Subject: [PATCH 064/230] Support TimeSeries commands with RESP 2 protocol (#3184) * Support Timeseries resp 2 * Change to resp 2 * Support Resp2 for TimeSeries commands --- command.go | 54 +- timeseries_commands_test.go | 2289 +++++++++++++++++++---------------- 2 files changed, 1270 insertions(+), 1073 deletions(-) diff --git a/command.go b/command.go index 4ced2979dc..7ea7862d5f 100644 --- a/command.go +++ b/command.go @@ -1403,27 +1403,63 @@ func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} { } func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadMapLen() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val = make(map[string][]interface{}, n) - for i := 0; i < n; i++ { - k, err := rd.ReadString() + + cmd.val = make(map[string][]interface{}) + + if readType == proto.RespMap { + n, err := rd.ReadMapLen() if err != nil { return err } - nn, err := rd.ReadArrayLen() + for i := 0; i < n; i++ { + k, err := rd.ReadString() + if err != nil { + return err + } + nn, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val[k] = make([]interface{}, nn) + for j := 0; j < nn; j++ { + value, err := rd.ReadReply() + if err != nil { + return err + } + cmd.val[k][j] = value + } + } + } else if readType == proto.RespArray { + // RESP2 response + n, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val[k] = make([]interface{}, nn) - for j := 0; j < nn; j++ { - value, err := rd.ReadReply() + + for i := 0; i < n; i++ { + // Each entry in this array is itself an array with key details + itemLen, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val[k][j] = value + + key, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[key] = make([]interface{}, 0, itemLen-1) + for j := 1; j < itemLen; j++ { + // Read the inner array for timestamp-value pairs + data, err := rd.ReadReply() + if err != nil { + return err + } + cmd.val[key] = append(cmd.val[key], data) + } } } diff --git a/timeseries_commands_test.go b/timeseries_commands_test.go index c62367a768..a2d4ba2936 100644 --- a/timeseries_commands_test.go +++ b/timeseries_commands_test.go @@ -2,6 +2,7 @@ package redis_test import ( "context" + "fmt" "strings" . "github.com/bsm/ginkgo/v2" @@ -12,1068 +13,1228 @@ import ( var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { ctx := context.TODO() - var client *redis.Client - - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: rediStackAddr}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - // Test TSCreateWithArgs - opt := &redis.TSOptions{Retention: 5} - result, err = client.TSCreateWithArgs(ctx, "2", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} - result, err = client.TSCreateWithArgs(ctx, "3", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Time": "Series"}, Retention: 20} - result, err = client.TSCreateWithArgs(ctx, "4", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - // Test chunk size - opt = &redis.TSOptions{ChunkSize: 128} - result, err = client.TSCreateWithArgs(ctx, "ts-cs-1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - // Test duplicate policy - duplicate_policies := []string{"BLOCK", "LAST", "FIRST", "MIN", "MAX"} - for _, dup := range duplicate_policies { - keyName := "ts-dup-" + dup - opt = &redis.TSOptions{DuplicatePolicy: dup} - result, err = client.TSCreateWithArgs(ctx, keyName, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err = client.TSInfo(ctx, keyName).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(strings.ToUpper(resultInfo["duplicatePolicy"].(string))).To(BeEquivalentTo(dup)) - } - // Test insertion filters - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: "LAST", IgnoreMaxValDiff: 10.0} - result, err = client.TSCreateWithArgs(ctx, "ts-if-1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1000)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1020, 11.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1020)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1021, 22.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1021)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1021).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(4)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1020, Value: 11.5}, - {Timestamp: 1021, Value: 22.0}})) - // Test insertion filters with other duplicate policy - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0} - result, err = client.TSCreateWithArgs(ctx, "ts-if-2", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultAdd1, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1000)) - resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1010)) - resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1013)) - - rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(3)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1013, Value: 10.0}})) - }) - It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { - result, err := client.TSAdd(ctx, "1", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - // Test TSAddWithArgs - opt := &redis.TSOptions{Retention: 10} - result, err = client.TSAddWithArgs(ctx, "2", 2, 3, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(2)) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} - result, err = client.TSAddWithArgs(ctx, "3", 3, 2, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(3)) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs", "Time": "Series"}, Retention: 10} - result, err = client.TSAddWithArgs(ctx, "4", 4, 2, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(4)) - resultInfo, err := client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - // Test chunk size - opt = &redis.TSOptions{ChunkSize: 128} - result, err = client.TSAddWithArgs(ctx, "ts-cs-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - // Test duplicate policy - // LAST - opt = &redis.TSOptions{DuplicatePolicy: "LAST"} - result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err := client.TSGet(ctx, "tsal-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(10)) - // FIRST - opt = &redis.TSOptions{DuplicatePolicy: "FIRST"} - result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsaf-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(5)) - // MAX - opt = &redis.TSOptions{DuplicatePolicy: "MAX"} - result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsam-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(10)) - // MIN - opt = &redis.TSOptions{DuplicatePolicy: "MIN"} - result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsami-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(5)) - // Insertion filters - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1000, 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1000)) - - result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1004, 3.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1000)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) - }) - - It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(0)) - - opt := &redis.TSAlterOptions{Retention: 10} - resultAlter, err := client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"]).To(BeEquivalentTo(map[interface{}]interface{}{})) - - opt = &redis.TSAlterOptions{Labels: map[string]string{"Time": "Series"}} - resultAlter, err = client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) - opt = &redis.TSAlterOptions{DuplicatePolicy: "min"} - resultAlter, err = client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("min")) - // Test insertion filters - resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1000)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1013)) - - alterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - resultAlter, err = client.TSAlter(ctx, "ts-if-1", alterOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1015, 11.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1013)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(3)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1013, Value: 10.0}})) - }) - - It("should TSCreateRule and TSDeleteRule", Label("timeseries", "tscreaterule", "tsdeleterule"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - result, err = client.TSCreate(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - result, err = client.TSCreateRule(ctx, "1", "2", redis.Avg, 100).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - for i := 0; i < 50; i++ { - resultAdd, err := client.TSAdd(ctx, "1", 100+i*2, 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 + i*2)) - resultAdd, err = client.TSAdd(ctx, "1", 100+i*2+1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 + i*2 + 1)) - - } - resultAdd, err := client.TSAdd(ctx, "1", 100*2, 1.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 * 2)) - resultGet, err := client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(1.5)) - Expect(resultGet.Timestamp).To(BeEquivalentTo(100)) - - resultDeleteRule, err := client.TSDeleteRule(ctx, "1", "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultDeleteRule).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["rules"]).To(BeEquivalentTo(map[interface{}]interface{}{})) - }) - - It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSIncrBy(ctx, "1", 1).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err := client.TSGet(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Value).To(BeEquivalentTo(100)) - - for i := 0; i < 100; i++ { - _, err := client.TSDecrBy(ctx, "1", 1).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err = client.TSGet(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Value).To(BeEquivalentTo(0)) - - opt := &redis.TSIncrDecrOptions{Timestamp: 5} - _, err = client.TSIncrByWithArgs(ctx, "2", 1.5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(5)) - Expect(result.Value).To(BeEquivalentTo(1.5)) - - opt = &redis.TSIncrDecrOptions{Timestamp: 7} - _, err = client.TSIncrByWithArgs(ctx, "2", 2.25, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(7)) - Expect(result.Value).To(BeEquivalentTo(3.75)) - - opt = &redis.TSIncrDecrOptions{Timestamp: 15} - _, err = client.TSDecrByWithArgs(ctx, "2", 1.5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(15)) - Expect(result.Value).To(BeEquivalentTo(2.25)) - - // Test chunk size INCRBY - opt = &redis.TSIncrDecrOptions{ChunkSize: 128} - _, err = client.TSIncrByWithArgs(ctx, "3", 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TSInfo(ctx, "3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - - // Test chunk size DECRBY - opt = &redis.TSIncrDecrOptions{ChunkSize: 128} - _, err = client.TSDecrByWithArgs(ctx, "4", 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err = client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - - // Test insertion filters INCRBY - opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - res, err := client.TSIncrByWithArgs(ctx, "ts-if-1", 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) - - res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}})) - - // Test insertion filters DECRBY - opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}})) - - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}})) - }) - - It("should TSGet", Label("timeseries", "tsget"), func() { - opt := &redis.TSOptions{DuplicatePolicy: "max"} - resultGet, err := client.TSAddWithArgs(ctx, "foo", 2265985, 151, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo(2265985)) - result, err := client.TSGet(ctx, "foo").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(2265985)) - Expect(result.Value).To(BeEquivalentTo(151)) - }) - - It("should TSGet Latest", Label("timeseries", "tsgetlatest", "NonRedisEnterprise"), func() { - resultGet, err := client.TSCreate(ctx, "tsgl-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - resultGet, err = client.TSCreate(ctx, "tsgl-2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - - resultGet, err = client.TSCreateRule(ctx, "tsgl-1", "tsgl-2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - - Expect(resultGet).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "tsgl-1", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - result, errGet := client.TSGet(ctx, "tsgl-2").Result() - Expect(errGet).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(0)) - Expect(result.Value).To(BeEquivalentTo(4)) - result, errGet = client.TSGetWithArgs(ctx, "tsgl-2", &redis.TSGetOptions{Latest: true}).Result() - Expect(errGet).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(10)) - Expect(result.Value).To(BeEquivalentTo(8)) - }) - - It("should TSInfo", Label("timeseries", "tsinfo"), func() { - resultGet, err := client.TSAdd(ctx, "foo", 2265985, 151).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo(2265985)) - result, err := client.TSInfo(ctx, "foo").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["firstTimestamp"]).To(BeEquivalentTo(2265985)) - }) - - It("should TSMAdd", Label("timeseries", "tsmadd"), func() { - resultGet, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - ktvSlices := make([][]interface{}, 3) - for i := 0; i < 3; i++ { - ktvSlices[i] = make([]interface{}, 3) - ktvSlices[i][0] = "a" - for j := 1; j < 3; j++ { - ktvSlices[i][j] = (i + j) * j - } - } - result, err := client.TSMAdd(ctx, ktvSlices).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]int64{1, 2, 3})) - }) - - It("should TSMGet and TSMGetWithArgs", Label("timeseries", "tsmget", "tsmgetWithArgs", "NonRedisEnterprise"), func() { - opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "a", "*", 15).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", "*", 25).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.TSMGet(ctx, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo(15)) - Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo(25)) - mgetOpt := &redis.TSMGetOptions{WithLabels: true} - result, err = client.TSMGetWithArgs(ctx, []string{"Test=This"}, mgetOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "Taste": "That"})) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreateRule, err := client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - result, err = client.TSMGet(ctx, []string{"is_compaction=true"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), 4.0})) - mgetOpt = &redis.TSMGetOptions{Latest: true} - result, err = client.TSMGetWithArgs(ctx, []string{"is_compaction=true"}, mgetOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), 8.0})) - }) - - It("should TSQueryIndex", Label("timeseries", "tsqueryindex"), func() { - opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - result, err := client.TSQueryIndex(ctx, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - result, err = client.TSQueryIndex(ctx, []string{"Taste=That"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(1)) - }) - - It("should TSDel and TSRange", Label("timeseries", "tsdel", "tsrange"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - resultDelete, err := client.TSDel(ctx, "a", 0, 21).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultDelete).To(BeEquivalentTo(22)) - - resultRange, err := client.TSRange(ctx, "a", 0, 21).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange).To(BeEquivalentTo([]redis.TSTimestampValue{})) - - resultRange, err = client.TSRange(ctx, "a", 22, 22).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 22, Value: 1})) - }) - - It("should TSRange, TSRangeWithArgs", Label("timeseries", "tsrange", "tsrangeWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - - } - result, err := client.TSRange(ctx, "a", 0, 200).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(100)) - for i := 0; i < 100; i++ { - client.TSAdd(ctx, "a", i+200, float64(i%7)) - } - result, err = client.TSRange(ctx, "a", 0, 500).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(200)) - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - opt := &redis.TSRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 10}, {Timestamp: 10, Value: 1}})) - opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "5"} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 5}, {Timestamp: 5, Value: 6}})) - opt = &redis.TSRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 2.55}, {Timestamp: 10, Value: 3}})) - // Test Range Latest - resultCreate, err := client.TSCreate(ctx, "t1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreate, err = client.TSCreate(ctx, "t2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRule).To(BeEquivalentTo("OK")) - _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - resultRange, err := client.TSRange(ctx, "t1", 0, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 1, Value: 1})) - - opt = &redis.TSRangeOptions{Latest: true} - resultRange, err = client.TSRangeWithArgs(ctx, "t2", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - // Test Bucket Timestamp - resultCreate, err = client.TSCreate(ctx, "t3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} - resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 20, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - // Test Empty - _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} - resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(7)) - }) - - It("should TSRevRange, TSRevRangeWithArgs", Label("timeseries", "tsrevrange", "tsrevrangeWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - - } - result, err := client.TSRange(ctx, "a", 0, 200).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(100)) - for i := 0; i < 100; i++ { - client.TSAdd(ctx, "a", i+200, float64(i%7)) - } - result, err = client.TSRange(ctx, "a", 0, 500).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(200)) - - opt := &redis.TSRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(20)) - - opt = &redis.TSRevRangeOptions{Count: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(10)) - - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - opt = &redis.TSRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 1}, {Timestamp: 0, Value: 10}})) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "1"} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1, Value: 10}, {Timestamp: 0, Value: 1}})) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 3}, {Timestamp: 0, Value: 2.55}})) - // Test Range Latest - resultCreate, err := client.TSCreate(ctx, "t1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreate, err = client.TSCreate(ctx, "t2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRule).To(BeEquivalentTo("OK")) - _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - resultRange, err := client.TSRange(ctx, "t2", 0, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - opt = &redis.TSRevRangeOptions{Latest: true} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 8})) - resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 9, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - // Test Bucket Timestamp - resultCreate, err = client.TSCreate(ctx, "t3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 80, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - // Test Empty - _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(7)) - }) - - It("should TSMRange and TSMRangeWithArgs", Label("timeseries", "tsmrange", "tsmrangeWithArgs"), func() { - createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() - Expect(err).NotTo(HaveOccurred()) - } - - result, err := client.TSMRange(ctx, 0, 200, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) - // Test Count - mrangeOpt := &redis.TSMRangeOptions{Count: 10} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) - // Test Aggregation and BucketDuration - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSMRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) - // Test WithLabels - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - mrangeOpt = &redis.TSMRangeOptions{WithLabels: true} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) - // Test SelectedLabels - mrangeOpt = &redis.TSMRangeOptions{SelectedLabels: []interface{}{"team"}} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) - // Test FilterBy - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - mrangeOpt = &redis.TSMRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), 1.0}, []interface{}{int64(16), 2.0}})) - // Test GroupBy - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "sum"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(3), 6.0}})) - - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "max"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "team", Reducer: "min"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - // Test Align - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} - result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 10.0}, []interface{}{int64(10), 1.0}})) - - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 5} - result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 5.0}, []interface{}{int64(5), 6.0}})) - }) - - It("should TSMRangeWithArgs Latest", Label("timeseries", "tsmrangeWithArgs", "tsmrangelatest", "NonRedisEnterprise"), func() { - resultCreate, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - - _, err = client.TSAdd(ctx, "a", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - mrangeOpt := &redis.TSMRangeOptions{Latest: true} - result, err := client.TSMRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) - Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) - }) - It("should TSMRevRange and TSMRevRangeWithArgs", Label("timeseries", "tsmrevrange", "tsmrevrangeWithArgs"), func() { - createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err := client.TSMRevRange(ctx, 0, 200, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) - // Test Count - mrangeOpt := &redis.TSMRevRangeOptions{Count: 10} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) - // Test Aggregation and BucketDuration - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - // Test WithLabels - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - mrangeOpt = &redis.TSMRevRangeOptions{WithLabels: true} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) - // Test SelectedLabels - mrangeOpt = &redis.TSMRevRangeOptions{SelectedLabels: []interface{}{"team"}} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) - // Test FilterBy - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - mrangeOpt = &redis.TSMRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(16), 2.0}, []interface{}{int64(15), 1.0}})) - // Test GroupBy - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "sum"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 6.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(0), 0.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "max"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "team", Reducer: "min"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - // Test Align - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 1.0}, []interface{}{int64(0), 10.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 1} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), 10.0}, []interface{}{int64(0), 1.0}})) - }) - - It("should TSMRevRangeWithArgs Latest", Label("timeseries", "tsmrevrangeWithArgs", "tsmrevrangelatest", "NonRedisEnterprise"), func() { - resultCreate, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - - _, err = client.TSAdd(ctx, "a", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - mrangeOpt := &redis.TSMRevRangeOptions{Latest: true} - result, err := client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) - Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) - }) + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, + }) + } + + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol // capture loop variable for each context + + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client + + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } + }) + + It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + // Test TSCreateWithArgs + opt := &redis.TSOptions{Retention: 5} + result, err = client.TSCreateWithArgs(ctx, "2", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} + result, err = client.TSCreateWithArgs(ctx, "3", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Time": "Series"}, Retention: 20} + result, err = client.TSCreateWithArgs(ctx, "4", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"Time", "Series"})) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + } + // Test chunk size + opt = &redis.TSOptions{ChunkSize: 128} + result, err = client.TSCreateWithArgs(ctx, "ts-cs-1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + // Test duplicate policy + duplicate_policies := []string{"BLOCK", "LAST", "FIRST", "MIN", "MAX"} + for _, dup := range duplicate_policies { + keyName := "ts-dup-" + dup + opt = &redis.TSOptions{DuplicatePolicy: dup} + result, err = client.TSCreateWithArgs(ctx, keyName, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err = client.TSInfo(ctx, keyName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(strings.ToUpper(resultInfo["duplicatePolicy"].(string))).To(BeEquivalentTo(dup)) + } + // Test insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: "LAST", IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1020, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1020)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1021, 22.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1021)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1021).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(4)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1020, Value: 11.5}, + {Timestamp: 1021, Value: 22.0}})) + // Test insertion filters with other duplicate policy + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-2", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd1, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1000)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1010)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1013)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) + }) + It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { + result, err := client.TSAdd(ctx, "1", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + // Test TSAddWithArgs + opt := &redis.TSOptions{Retention: 10} + result, err = client.TSAddWithArgs(ctx, "2", 2, 3, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(2)) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} + result, err = client.TSAddWithArgs(ctx, "3", 3, 2, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(3)) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs", "Time": "Series"}, Retention: 10} + result, err = client.TSAddWithArgs(ctx, "4", 4, 2, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(4)) + resultInfo, err := client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})).To(ContainElement([]interface{}{"Time", "Series"})) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + } + // Test chunk size + opt = &redis.TSOptions{ChunkSize: 128} + result, err = client.TSAddWithArgs(ctx, "ts-cs-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + // Test duplicate policy + // LAST + opt = &redis.TSOptions{DuplicatePolicy: "LAST"} + result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err := client.TSGet(ctx, "tsal-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(10)) + // FIRST + opt = &redis.TSOptions{DuplicatePolicy: "FIRST"} + result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsaf-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(5)) + // MAX + opt = &redis.TSOptions{DuplicatePolicy: "MAX"} + result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsam-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(10)) + // MIN + opt = &redis.TSOptions{DuplicatePolicy: "MIN"} + result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsami-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(5)) + // Insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1000, 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1004, 3.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) + }) + + It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(0)) + + opt := &redis.TSAlterOptions{Retention: 10} + resultAlter, err := client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(resultInfo["labels"]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + + opt = &redis.TSAlterOptions{Labels: map[string]string{"Time": "Series"}} + resultAlter, err = client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"Time", "Series"})) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } + opt = &redis.TSAlterOptions{DuplicatePolicy: "min"} + resultAlter, err = client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("min")) + // Test insertion filters + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + alterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + resultAlter, err = client.TSAlter(ctx, "ts-if-1", alterOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1015, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) + }) + + It("should TSCreateRule and TSDeleteRule", Label("timeseries", "tscreaterule", "tsdeleterule"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + result, err = client.TSCreate(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + result, err = client.TSCreateRule(ctx, "1", "2", redis.Avg, 100).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + for i := 0; i < 50; i++ { + resultAdd, err := client.TSAdd(ctx, "1", 100+i*2, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 + i*2)) + resultAdd, err = client.TSAdd(ctx, "1", 100+i*2+1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 + i*2 + 1)) + + } + resultAdd, err := client.TSAdd(ctx, "1", 100*2, 1.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 * 2)) + resultGet, err := client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(1.5)) + Expect(resultGet.Timestamp).To(BeEquivalentTo(100)) + + resultDeleteRule, err := client.TSDeleteRule(ctx, "1", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultDeleteRule).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["rules"]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(resultInfo["rules"]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + }) + + It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSIncrBy(ctx, "1", 1).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err := client.TSGet(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Value).To(BeEquivalentTo(100)) + + for i := 0; i < 100; i++ { + _, err := client.TSDecrBy(ctx, "1", 1).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err = client.TSGet(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Value).To(BeEquivalentTo(0)) + + opt := &redis.TSIncrDecrOptions{Timestamp: 5} + _, err = client.TSIncrByWithArgs(ctx, "2", 1.5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(5)) + Expect(result.Value).To(BeEquivalentTo(1.5)) + + opt = &redis.TSIncrDecrOptions{Timestamp: 7} + _, err = client.TSIncrByWithArgs(ctx, "2", 2.25, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(7)) + Expect(result.Value).To(BeEquivalentTo(3.75)) + + opt = &redis.TSIncrDecrOptions{Timestamp: 15} + _, err = client.TSDecrByWithArgs(ctx, "2", 1.5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(15)) + Expect(result.Value).To(BeEquivalentTo(2.25)) + + // Test chunk size INCRBY + opt = &redis.TSIncrDecrOptions{ChunkSize: 128} + _, err = client.TSIncrByWithArgs(ctx, "3", 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TSInfo(ctx, "3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + + // Test chunk size DECRBY + opt = &redis.TSIncrDecrOptions{ChunkSize: 128} + _, err = client.TSDecrByWithArgs(ctx, "4", 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err = client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + + // Test insertion filters INCRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err := client.TSIncrByWithArgs(ctx, "ts-if-1", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}})) + + // Test insertion filters DECRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}})) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}})) + }) + + It("should TSGet", Label("timeseries", "tsget"), func() { + opt := &redis.TSOptions{DuplicatePolicy: "max"} + resultGet, err := client.TSAddWithArgs(ctx, "foo", 2265985, 151, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo(2265985)) + result, err := client.TSGet(ctx, "foo").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(2265985)) + Expect(result.Value).To(BeEquivalentTo(151)) + }) + + It("should TSGet Latest", Label("timeseries", "tsgetlatest", "NonRedisEnterprise"), func() { + resultGet, err := client.TSCreate(ctx, "tsgl-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + resultGet, err = client.TSCreate(ctx, "tsgl-2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + + resultGet, err = client.TSCreateRule(ctx, "tsgl-1", "tsgl-2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + + Expect(resultGet).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "tsgl-1", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + result, errGet := client.TSGet(ctx, "tsgl-2").Result() + Expect(errGet).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(0)) + Expect(result.Value).To(BeEquivalentTo(4)) + result, errGet = client.TSGetWithArgs(ctx, "tsgl-2", &redis.TSGetOptions{Latest: true}).Result() + Expect(errGet).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(10)) + Expect(result.Value).To(BeEquivalentTo(8)) + }) + + It("should TSInfo", Label("timeseries", "tsinfo"), func() { + resultGet, err := client.TSAdd(ctx, "foo", 2265985, 151).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo(2265985)) + result, err := client.TSInfo(ctx, "foo").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result["firstTimestamp"]).To(BeEquivalentTo(2265985)) + }) + + It("should TSMAdd", Label("timeseries", "tsmadd"), func() { + resultGet, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + ktvSlices := make([][]interface{}, 3) + for i := 0; i < 3; i++ { + ktvSlices[i] = make([]interface{}, 3) + ktvSlices[i][0] = "a" + for j := 1; j < 3; j++ { + ktvSlices[i][j] = (i + j) * j + } + } + result, err := client.TSMAdd(ctx, ktvSlices).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]int64{1, 2, 3})) + }) + + It("should TSMGet and TSMGetWithArgs", Label("timeseries", "tsmget", "tsmgetWithArgs", "NonRedisEnterprise"), func() { + opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "a", "*", 15).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", "*", 25).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.TSMGet(ctx, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo("15")) + Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo("25")) + } else { + Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo(15)) + Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo(25)) + } + mgetOpt := &redis.TSMGetOptions{WithLabels: true} + result, err = client.TSMGetWithArgs(ctx, []string{"Test=This"}, mgetOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][0]).To(ConsistOf([]interface{}{"Test", "This"}, []interface{}{"Taste", "That"})) + } else { + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "Taste": "That"})) + } + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreateRule, err := client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + result, err = client.TSMGet(ctx, []string{"is_compaction=true"}).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), "4"})) + } else { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), 4.0})) + } + mgetOpt = &redis.TSMGetOptions{Latest: true} + result, err = client.TSMGetWithArgs(ctx, []string{"is_compaction=true"}, mgetOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), "8"})) + } else { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), 8.0})) + } + }) + + It("should TSQueryIndex", Label("timeseries", "tsqueryindex"), func() { + opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + result, err := client.TSQueryIndex(ctx, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + result, err = client.TSQueryIndex(ctx, []string{"Taste=That"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(1)) + }) + + It("should TSDel and TSRange", Label("timeseries", "tsdel", "tsrange"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + resultDelete, err := client.TSDel(ctx, "a", 0, 21).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultDelete).To(BeEquivalentTo(22)) + + resultRange, err := client.TSRange(ctx, "a", 0, 21).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange).To(BeEquivalentTo([]redis.TSTimestampValue{})) + + resultRange, err = client.TSRange(ctx, "a", 22, 22).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 22, Value: 1})) + }) + + It("should TSRange, TSRangeWithArgs", Label("timeseries", "tsrange", "tsrangeWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + + } + result, err := client.TSRange(ctx, "a", 0, 200).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(100)) + for i := 0; i < 100; i++ { + client.TSAdd(ctx, "a", i+200, float64(i%7)) + } + result, err = client.TSRange(ctx, "a", 0, 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(200)) + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + opt := &redis.TSRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 10}, {Timestamp: 10, Value: 1}})) + opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "5"} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 5}, {Timestamp: 5, Value: 6}})) + opt = &redis.TSRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 2.55}, {Timestamp: 10, Value: 3}})) + // Test Range Latest + resultCreate, err := client.TSCreate(ctx, "t1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreate, err = client.TSCreate(ctx, "t2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRule).To(BeEquivalentTo("OK")) + _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + resultRange, err := client.TSRange(ctx, "t1", 0, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 1, Value: 1})) + + opt = &redis.TSRangeOptions{Latest: true} + resultRange, err = client.TSRangeWithArgs(ctx, "t2", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + // Test Bucket Timestamp + resultCreate, err = client.TSCreate(ctx, "t3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} + resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 20, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + // Test Empty + _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} + resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(7)) + }) + + It("should TSRevRange, TSRevRangeWithArgs", Label("timeseries", "tsrevrange", "tsrevrangeWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + + } + result, err := client.TSRange(ctx, "a", 0, 200).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(100)) + for i := 0; i < 100; i++ { + client.TSAdd(ctx, "a", i+200, float64(i%7)) + } + result, err = client.TSRange(ctx, "a", 0, 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(200)) + + opt := &redis.TSRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(20)) + + opt = &redis.TSRevRangeOptions{Count: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(10)) + + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + opt = &redis.TSRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 1}, {Timestamp: 0, Value: 10}})) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "1"} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1, Value: 10}, {Timestamp: 0, Value: 1}})) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 3}, {Timestamp: 0, Value: 2.55}})) + // Test Range Latest + resultCreate, err := client.TSCreate(ctx, "t1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreate, err = client.TSCreate(ctx, "t2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRule).To(BeEquivalentTo("OK")) + _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + resultRange, err := client.TSRange(ctx, "t2", 0, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + opt = &redis.TSRevRangeOptions{Latest: true} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 8})) + resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 9, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + // Test Bucket Timestamp + resultCreate, err = client.TSCreate(ctx, "t3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 80, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + // Test Empty + _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(7)) + }) + + It("should TSMRange and TSMRangeWithArgs", Label("timeseries", "tsmrange", "tsmrangeWithArgs"), func() { + createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + result, err := client.TSMRange(ctx, 0, 200, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(100)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) + } + // Test Count + mrangeOpt := &redis.TSMRangeOptions{Count: 10} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(10)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) + } + // Test Aggregation and BucketDuration + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSMRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(20)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) + } + // Test WithLabels + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + mrangeOpt = &redis.TSMRangeOptions{WithLabels: true} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(ConsistOf([]interface{}{[]interface{}{"Test", "This"}, []interface{}{"team", "ny"}})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) + } + // Test SelectedLabels + mrangeOpt = &redis.TSMRangeOptions{SelectedLabels: []interface{}{"team"}} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "ny"})) + Expect(result["b"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "sf"})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) + } + // Test FilterBy + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + mrangeOpt = &redis.TSMRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), "1"}, []interface{}{int64(16), "2"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), 1.0}, []interface{}{int64(16), 2.0}})) + } + // Test GroupBy + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "sum"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "2"}, []interface{}{int64(2), "4"}, []interface{}{int64(3), "6"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(3), 6.0}})) + } + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "max"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + } + + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "team", Reducer: "min"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(result["team=ny"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + Expect(result["team=sf"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + } else { + Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + } + // Test Align + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} + result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "10"}, []interface{}{int64(10), "1"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 10.0}, []interface{}{int64(10), 1.0}})) + } + + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 5} + result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "5"}, []interface{}{int64(5), "6"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 5.0}, []interface{}{int64(5), 6.0}})) + } + }) + + It("should TSMRangeWithArgs Latest", Label("timeseries", "tsmrangeWithArgs", "tsmrangelatest", "NonRedisEnterprise"), func() { + resultCreate, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + + _, err = client.TSAdd(ctx, "a", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + mrangeOpt := &redis.TSMRangeOptions{Latest: true} + result, err := client.TSMRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][1]).To(ConsistOf([]interface{}{int64(0), "4"}, []interface{}{int64(10), "8"})) + Expect(result["d"][1]).To(ConsistOf([]interface{}{int64(0), "4"}, []interface{}{int64(10), "8"})) + } else { + Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) + Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) + } + }) + It("should TSMRevRange and TSMRevRangeWithArgs", Label("timeseries", "tsmrevrange", "tsmrevrangeWithArgs"), func() { + createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err := client.TSMRevRange(ctx, 0, 200, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(100)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) + } + // Test Count + mrangeOpt := &redis.TSMRevRangeOptions{Count: 10} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(10)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) + } + // Test Aggregation and BucketDuration + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(20)) + Expect(result["a"][0]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + mrangeOpt = &redis.TSMRevRangeOptions{WithLabels: true} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(ConsistOf([]interface{}{[]interface{}{"Test", "This"}, []interface{}{"team", "ny"}})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) + } + // Test SelectedLabels + mrangeOpt = &redis.TSMRevRangeOptions{SelectedLabels: []interface{}{"team"}} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "ny"})) + Expect(result["b"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "sf"})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) + } + // Test FilterBy + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + mrangeOpt = &redis.TSMRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})).To(ConsistOf([]interface{}{int64(16), "2"}, []interface{}{int64(15), "1"})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(16), 2.0}, []interface{}{int64(15), 1.0}})) + } + // Test GroupBy + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "sum"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "6"}, []interface{}{int64(2), "4"}, []interface{}{int64(1), "2"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 6.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(0), 0.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "max"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "team", Reducer: "min"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(result["team=ny"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + Expect(result["team=sf"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + } + // Test Align + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "1"}, []interface{}{int64(0), "10"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 1.0}, []interface{}{int64(0), 10.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 1} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), "10"}, []interface{}{int64(0), "1"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), 10.0}, []interface{}{int64(0), 1.0}})) + } + }) + + It("should TSMRevRangeWithArgs Latest", Label("timeseries", "tsmrevrangeWithArgs", "tsmrevrangelatest", "NonRedisEnterprise"), func() { + resultCreate, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + + _, err = client.TSAdd(ctx, "a", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + mrangeOpt := &redis.TSMRevRangeOptions{Latest: true} + result, err := client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "8"}, []interface{}{int64(0), "4"}})) + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "8"}, []interface{}{int64(0), "4"}})) + } else { + Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) + Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) + } + }) + }) + } }) From 92b675f721b45978b3a995782e3a4eea4a6c5429 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:15:19 +0200 Subject: [PATCH 065/230] Support Probabilistic commands with RESP 2 protocol (#3176) * Support bloom resp 2 * Support Resp2 for BF.Info * simplify BFInfoCmd field assignment using map-based key-to-field references --- probabilistic.go | 72 ++- probabilistic_test.go | 1437 +++++++++++++++++++++-------------------- 2 files changed, 780 insertions(+), 729 deletions(-) diff --git a/probabilistic.go b/probabilistic.go index 5d5cd1a628..02ca263cbd 100644 --- a/probabilistic.go +++ b/probabilistic.go @@ -319,37 +319,69 @@ func (cmd *BFInfoCmd) Result() (BFInfo, error) { } func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadMapLen() + result := BFInfo{} + + // Create a mapping from key names to pointers of struct fields + respMapping := map[string]*int64{ + "Capacity": &result.Capacity, + "CAPACITY": &result.Capacity, + "Size": &result.Size, + "SIZE": &result.Size, + "Number of filters": &result.Filters, + "FILTERS": &result.Filters, + "Number of items inserted": &result.ItemsInserted, + "ITEMS": &result.ItemsInserted, + "Expansion rate": &result.ExpansionRate, + "EXPANSION": &result.ExpansionRate, + } + + // Helper function to read and assign a value based on the key + readAndAssignValue := func(key string) error { + fieldPtr, exists := respMapping[key] + if !exists { + return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key) + } + + // Read the integer and assign to the field via pointer dereferencing + val, err := rd.ReadInt() + if err != nil { + return err + } + *fieldPtr = val + return nil + } + + readType, err := rd.PeekReplyType() if err != nil { return err } - var key string - var result BFInfo - for f := 0; f < n; f++ { - key, err = rd.ReadString() + if len(cmd.args) > 2 && readType == proto.RespArray { + n, err := rd.ReadArrayLen() if err != nil { return err } - - switch key { - case "Capacity": - result.Capacity, err = rd.ReadInt() - case "Size": - result.Size, err = rd.ReadInt() - case "Number of filters": - result.Filters, err = rd.ReadInt() - case "Number of items inserted": - result.ItemsInserted, err = rd.ReadInt() - case "Expansion rate": - result.ExpansionRate, err = rd.ReadInt() - default: - return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key) + if key, ok := cmd.args[2].(string); ok && n == 1 { + if err := readAndAssignValue(key); err != nil { + return err + } + } else { + return fmt.Errorf("redis: BLOOM.INFO invalid argument key type") } - + } else { + n, err := rd.ReadMapLen() if err != nil { return err } + for i := 0; i < n; i++ { + key, err := rd.ReadString() + if err != nil { + return err + } + if err := readAndAssignValue(key); err != nil { + return err + } + } } cmd.val = result diff --git a/probabilistic_test.go b/probabilistic_test.go index 0610c515ec..a0a050e23e 100644 --- a/probabilistic_test.go +++ b/probabilistic_test.go @@ -13,721 +13,740 @@ import ( var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { ctx := context.TODO() - var client *redis.Client - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - Describe("bloom", Label("bloom"), func() { - It("should BFAdd", Label("bloom", "bfadd"), func() { - resultAdd, err := client.BFAdd(ctx, "testbf1", 1).Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeTrue()) - - resultInfo, err := client.BFInfo(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(1))) - }) - - It("should BFCard", Label("bloom", "bfcard"), func() { - // This is a probabilistic data structure, and it's not always guaranteed that we will get back - // the exact number of inserted items, during hash collisions - // But with such a low number of items (only 3), - // the probability of a collision is very low, so we can expect to get back the exact number of items - _, err := client.BFAdd(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.BFAdd(ctx, "testbf1", "item2").Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.BFAdd(ctx, "testbf1", 3).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFCard(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(int64(3))) - }) - - It("should BFExists", Label("bloom", "bfexists"), func() { - exists, err := client.BFExists(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - - _, err = client.BFAdd(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - - exists, err = client.BFExists(ctx, "testbf1", "item1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - }) - - It("should BFInfo and BFReserve", Label("bloom", "bfinfo", "bfreserve"), func() { - err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - }) - - It("should BFInfoCapacity, BFInfoSize, BFInfoFilters, BFInfoItems, BFInfoExpansion, ", Label("bloom", "bfinfocapacity", "bfinfosize", "bfinfofilters", "bfinfoitems", "bfinfoexpansion"), func() { - err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfoCapacity(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - - result, err = client.BFInfoItems(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.ItemsInserted).To(BeEquivalentTo(int64(0))) - - result, err = client.BFInfoSize(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Size).To(BeEquivalentTo(int64(4056))) - - err = client.BFReserveExpansion(ctx, "testbf2", 0.001, 2000, 3).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.BFInfoFilters(ctx, "testbf2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Filters).To(BeEquivalentTo(int64(1))) - - result, err = client.BFInfoExpansion(ctx, "testbf2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFInsert", Label("bloom", "bfinsert"), func() { - options := &redis.BFInsertOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - NoCreate: true, - } - - resultInsert, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("ERR not found")) - - options = &redis.BFInsertOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - NoCreate: false, - } - - resultInsert, err = client.BFInsert(ctx, "testbf1", options, "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultInsert)).To(BeEquivalentTo(1)) - - exists, err := client.BFExists(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFMAdd", Label("bloom", "bfmadd"), func() { - resultAdd, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultAdd)).To(Equal(3)) - - resultInfo, err := client.BFInfo(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(3))) - resultAdd2, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd2[0]).To(BeFalse()) - Expect(resultAdd2[1]).To(BeFalse()) - Expect(resultAdd2[2]).To(BeTrue()) - }) - - It("should BFMExists", Label("bloom", "bfmexists"), func() { - exist, err := client.BFMExists(ctx, "testbf1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(exist)).To(Equal(3)) - Expect(exist[0]).To(BeFalse()) - Expect(exist[1]).To(BeFalse()) - Expect(exist[2]).To(BeFalse()) - - _, err = client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - - exist, err = client.BFMExists(ctx, "testbf1", "item1", "item2", "item3", "item4").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(len(exist)).To(Equal(4)) - Expect(exist[0]).To(BeTrue()) - Expect(exist[1]).To(BeTrue()) - Expect(exist[2]).To(BeTrue()) - Expect(exist[3]).To(BeFalse()) - }) - - It("should BFReserveExpansion", Label("bloom", "bfreserveexpansion"), func() { - err := client.BFReserveExpansion(ctx, "testbf1", 0.001, 2000, 3).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFReserveNonScaling", Label("bloom", "bfreservenonscaling"), func() { - err := client.BFReserveNonScaling(ctx, "testbfns1", 0.001, 1000).Err() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.BFInfo(ctx, "testbfns1").Result() - Expect(err).To(HaveOccurred()) - }) - - It("should BFScanDump and BFLoadChunk", Label("bloom", "bfscandump", "bfloadchunk"), func() { - err := client.BFReserve(ctx, "testbfsd1", 0.001, 3000).Err() - Expect(err).NotTo(HaveOccurred()) - for i := 0; i < 1000; i++ { - client.BFAdd(ctx, "testbfsd1", i) - } - infBefore := client.BFInfoSize(ctx, "testbfsd1") - fd := []redis.ScanDump{} - sd, err := client.BFScanDump(ctx, "testbfsd1", 0).Result() - for { - if sd.Iter == 0 { - break - } - Expect(err).NotTo(HaveOccurred()) - fd = append(fd, sd) - sd, err = client.BFScanDump(ctx, "testbfsd1", sd.Iter).Result() - } - client.Del(ctx, "testbfsd1") - for _, e := range fd { - client.BFLoadChunk(ctx, "testbfsd1", e.Iter, e.Data) - } - infAfter := client.BFInfoSize(ctx, "testbfsd1") - Expect(infBefore).To(BeEquivalentTo(infAfter)) - }) - - It("should BFReserveWithArgs", Label("bloom", "bfreserveargs"), func() { - options := &redis.BFReserveOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - } - err := client.BFReserveWithArgs(ctx, "testbf", options).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - }) - - Describe("cuckoo", Label("cuckoo"), func() { - It("should CFAdd", Label("cuckoo", "cfadd"), func() { - add, err := client.CFAdd(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(add).To(BeTrue()) - - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - info, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, }) + } - It("should CFAddNX", Label("cuckoo", "cfaddnx"), func() { - add, err := client.CFAddNX(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(add).To(BeTrue()) + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol // capture loop variable for each context - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client - result, err := client.CFAddNX(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeFalse()) + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) - info, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) - }) - - It("should CFCount", Label("cuckoo", "cfcount"), func() { - err := client.CFAdd(ctx, "testcf1", "item1").Err() - cnt, err := client.CFCount(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cnt).To(BeEquivalentTo(int64(1))) - - err = client.CFAdd(ctx, "testcf1", "item1").Err() - Expect(err).NotTo(HaveOccurred()) - - cnt, err = client.CFCount(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cnt).To(BeEquivalentTo(int64(2))) - }) - - It("should CFDel and CFExists", Label("cuckoo", "cfdel", "cfexists"), func() { - err := client.CFAdd(ctx, "testcf1", "item1").Err() - Expect(err).NotTo(HaveOccurred()) - - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - del, err := client.CFDel(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(del).To(BeTrue()) - - exists, err = client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should CFInfo and CFReserve", Label("cuckoo", "cfinfo", "cfreserve"), func() { - err := client.CFReserve(ctx, "testcf1", 1000).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveExpansion(ctx, "testcfe1", 1000, 1).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveBucketSize(ctx, "testcfbs1", 1000, 4).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveMaxIterations(ctx, "testcfmi1", 1000, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) - }) - - It("should CFScanDump and CFLoadChunk", Label("bloom", "cfscandump", "cfloadchunk"), func() { - err := client.CFReserve(ctx, "testcfsd1", 1000).Err() - Expect(err).NotTo(HaveOccurred()) - for i := 0; i < 1000; i++ { - Item := fmt.Sprintf("item%d", i) - client.CFAdd(ctx, "testcfsd1", Item) - } - infBefore := client.CFInfo(ctx, "testcfsd1") - fd := []redis.ScanDump{} - sd, err := client.CFScanDump(ctx, "testcfsd1", 0).Result() - for { - if sd.Iter == 0 { - break + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() } - Expect(err).NotTo(HaveOccurred()) - fd = append(fd, sd) - sd, err = client.CFScanDump(ctx, "testcfsd1", sd.Iter).Result() - } - client.Del(ctx, "testcfsd1") - for _, e := range fd { - client.CFLoadChunk(ctx, "testcfsd1", e.Iter, e.Data) - } - infAfter := client.CFInfo(ctx, "testcfsd1") - Expect(infBefore).To(BeEquivalentTo(infAfter)) - }) - - It("should CFInfo and CFReserveWithArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { - args := &redis.CFReserveOptions{ - Capacity: 2048, - BucketSize: 3, - MaxIterations: 15, - Expansion: 2, - } - - err := client.CFReserveWithArgs(ctx, "testcf1", args).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(result.BucketSize).To(BeEquivalentTo(int64(3))) - Expect(result.MaxIteration).To(BeEquivalentTo(int64(15))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(2))) - }) - - It("should CFInsert", Label("cuckoo", "cfinsert"), func() { - args := &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: true, - } - - result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() - Expect(err).To(HaveOccurred()) - - args = &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: false, - } - - result, err = client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - }) - - It("should CFInsertNX", Label("cuckoo", "cfinsertnx"), func() { - args := &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: true, - } - - result, err := client.CFInsertNX(ctx, "testcf1", args, "item1", "item2", "item2").Result() - Expect(err).To(HaveOccurred()) - - args = &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: false, - } - - result, err = client.CFInsertNX(ctx, "testcf2", args, "item1", "item2", "item2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(1))) - Expect(result[2]).To(BeEquivalentTo(int64(0))) - }) - - It("should CFMexists", Label("cuckoo", "cfmexists"), func() { - err := client.CFInsert(ctx, "testcf1", nil, "item1", "item2", "item3").Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFMExists(ctx, "testcf1", "item1", "item2", "item3", "item4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(4)) - Expect(result[0]).To(BeTrue()) - Expect(result[1]).To(BeTrue()) - Expect(result[2]).To(BeTrue()) - Expect(result[3]).To(BeFalse()) - }) - }) - - Describe("CMS", Label("cms"), func() { - It("should CMSIncrBy", Label("cms", "cmsincrby"), func() { - err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CMSIncrBy(ctx, "testcms1", "item1", 1, "item2", 2, "item3", 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(2))) - Expect(result[2]).To(BeEquivalentTo(int64(3))) - }) - - It("should CMSInitByDim and CMSInfo", Label("cms", "cmsinitbydim", "cmsinfo"), func() { - err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.CMSInfo(ctx, "testcms1").Result() - Expect(err).NotTo(HaveOccurred()) - - Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) - Expect(info.Width).To(BeEquivalentTo(int64(5))) - Expect(info.Depth).To(BeEquivalentTo(int64(10))) - }) - - It("should CMSInitByProb", Label("cms", "cmsinitbyprob"), func() { - err := client.CMSInitByProb(ctx, "testcms1", 0.002, 0.01).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.CMSInfo(ctx, "testcms1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) - }) - - It("should CMSMerge, CMSMergeWithWeight and CMSQuery", Label("cms", "cmsmerge", "cmsquery", "NonRedisEnterprise"), func() { - err := client.CMSMerge(ctx, "destCms1", "testcms2", "testcms3").Err() - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("CMS: key does not exist")) - - err = client.CMSInitByDim(ctx, "destCms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "destCms2", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms1", 2, 20).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms2", 3, 20).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() - Expect(err).To(MatchError("CMS: width/depth is not equal")) - - client.Del(ctx, "cms1", "cms2") - - err = client.CMSInitByDim(ctx, "cms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms2", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - client.CMSIncrBy(ctx, "cms1", "item1", 1, "item2", 2) - client.CMSIncrBy(ctx, "cms2", "item2", 2, "item3", 3) - - err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CMSQuery(ctx, "destCms1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(4))) - Expect(result[2]).To(BeEquivalentTo(int64(3))) - - sourceSketches := map[string]int64{ - "cms1": 1, - "cms2": 2, - } - err = client.CMSMergeWithWeight(ctx, "destCms2", sourceSketches).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.CMSQuery(ctx, "destCms2", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(6))) - Expect(result[2]).To(BeEquivalentTo(int64(6))) - }) - }) - - Describe("TopK", Label("topk"), func() { - It("should TopKReserve, TopKInfo, TopKAdd, TopKQuery, TopKCount, TopKIncrBy, TopKList, TopKListWithCount", Label("topk", "topkreserve", "topkinfo", "topkadd", "topkquery", "topkcount", "topkincrby", "topklist", "topklistwithcount"), func() { - err := client.TopKReserve(ctx, "topk1", 3).Err() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TopKInfo(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) - - resultAdd, err := client.TopKAdd(ctx, "topk1", "item1", "item2", 3, "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultAdd)).To(BeEquivalentTo(int64(4))) - - resultQuery, err := client.TopKQuery(ctx, "topk1", "item1", "item2", 4, 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultQuery)).To(BeEquivalentTo(4)) - Expect(resultQuery[0]).To(BeTrue()) - Expect(resultQuery[1]).To(BeTrue()) - Expect(resultQuery[2]).To(BeFalse()) - Expect(resultQuery[3]).To(BeTrue()) - - resultCount, err := client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultCount)).To(BeEquivalentTo(3)) - Expect(resultCount[0]).To(BeEquivalentTo(int64(2))) - Expect(resultCount[1]).To(BeEquivalentTo(int64(1))) - Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) - - resultIncr, err := client.TopKIncrBy(ctx, "topk1", "item1", 5, "item2", 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultIncr)).To(BeEquivalentTo(2)) - - resultCount, err = client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultCount)).To(BeEquivalentTo(3)) - Expect(resultCount[0]).To(BeEquivalentTo(int64(7))) - Expect(resultCount[1]).To(BeEquivalentTo(int64(11))) - Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) - - resultList, err := client.TopKList(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultList)).To(BeEquivalentTo(3)) - Expect(resultList).To(ContainElements("item2", "item1", "3")) - - resultListWithCount, err := client.TopKListWithCount(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultListWithCount)).To(BeEquivalentTo(3)) - Expect(resultListWithCount["3"]).To(BeEquivalentTo(int64(1))) - Expect(resultListWithCount["item1"]).To(BeEquivalentTo(int64(7))) - Expect(resultListWithCount["item2"]).To(BeEquivalentTo(int64(11))) - }) - - It("should TopKReserveWithOptions", Label("topk", "topkreservewithoptions"), func() { - err := client.TopKReserveWithOptions(ctx, "topk1", 3, 1500, 8, 0.5).Err() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TopKInfo(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) - Expect(resultInfo.Width).To(BeEquivalentTo(int64(1500))) - Expect(resultInfo.Depth).To(BeEquivalentTo(int64(8))) - Expect(resultInfo.Decay).To(BeEquivalentTo(0.5)) - }) - }) - - Describe("t-digest", Label("tdigest"), func() { - It("should TDigestAdd, TDigestCreate, TDigestInfo, TDigestByRank, TDigestByRevRank, TDigestCDF, TDigestMax, TDigestMin, TDigestQuantile, TDigestRank, TDigestRevRank, TDigestTrimmedMean, TDigestReset, ", Label("tdigest", "tdigestadd", "tdigestcreate", "tdigestinfo", "tdigestbyrank", "tdigestbyrevrank", "tdigestcdf", "tdigestmax", "tdigestmin", "tdigestquantile", "tdigestrank", "tdigestrevrank", "tdigesttrimmedmean", "tdigestreset"), func() { - err := client.TDigestCreate(ctx, "tdigest1").Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(0))) - - // Test with empty sketch - byRank, err := client.TDigestByRank(ctx, "tdigest1", 0, 1, 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRank)).To(BeEquivalentTo(4)) - - byRevRank, err := client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRevRank)).To(BeEquivalentTo(3)) - - cdf, err := client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(cdf)).To(BeEquivalentTo(3)) - - max, err := client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(max)).To(BeTrue()) - - min, err := client.TDigestMin(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(min)).To(BeTrue()) - - quantile, err := client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(quantile)).To(BeEquivalentTo(2)) - - rank, err := client.TDigestRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rank)).To(BeEquivalentTo(2)) - - revRank, err := client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(revRank)).To(BeEquivalentTo(2)) - - trimmedMean, err := client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(trimmedMean)).To(BeTrue()) - - // Add elements - err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err = client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(10))) - - byRank, err = client.TDigestByRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRank)).To(BeEquivalentTo(3)) - Expect(byRank[0]).To(BeEquivalentTo(float64(10))) - Expect(byRank[1]).To(BeEquivalentTo(float64(20))) - Expect(byRank[2]).To(BeEquivalentTo(float64(30))) - - byRevRank, err = client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRevRank)).To(BeEquivalentTo(3)) - Expect(byRevRank[0]).To(BeEquivalentTo(float64(100))) - Expect(byRevRank[1]).To(BeEquivalentTo(float64(90))) - Expect(byRevRank[2]).To(BeEquivalentTo(float64(80))) - - cdf, err = client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(cdf)).To(BeEquivalentTo(3)) - Expect(cdf[0]).To(BeEquivalentTo(0.1)) - Expect(cdf[1]).To(BeEquivalentTo(0.3)) - Expect(cdf[2]).To(BeEquivalentTo(0.65)) - - max, err = client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(max).To(BeEquivalentTo(float64(100))) - - min, err = client.TDigestMin(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(min).To(BeEquivalentTo(float64(10))) - - quantile, err = client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(quantile)).To(BeEquivalentTo(2)) - Expect(quantile[0]).To(BeEquivalentTo(float64(20))) - Expect(quantile[1]).To(BeEquivalentTo(float64(30))) - - rank, err = client.TDigestRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rank)).To(BeEquivalentTo(2)) - Expect(rank[0]).To(BeEquivalentTo(int64(0))) - Expect(rank[1]).To(BeEquivalentTo(int64(1))) - - revRank, err = client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(revRank)).To(BeEquivalentTo(2)) - Expect(revRank[0]).To(BeEquivalentTo(int64(9))) - Expect(revRank[1]).To(BeEquivalentTo(int64(8))) - - trimmedMean, err = client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(trimmedMean).To(BeEquivalentTo(float64(40))) - - reset, err := client.TDigestReset(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(reset).To(BeEquivalentTo("OK")) - }) - - It("should TDigestCreateWithCompression", Label("tdigest", "tcreatewithcompression"), func() { - err := client.TDigestCreateWithCompression(ctx, "tdigest1", 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Compression).To(BeEquivalentTo(int64(2000))) - }) - - It("should TDigestMerge", Label("tdigest", "tmerge", "NonRedisEnterprise"), func() { - err := client.TDigestCreate(ctx, "tdigest1").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.TDigestCreate(ctx, "tdigest2").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest2", 15, 25, 35, 45, 55, 65, 75, 85, 95, 105).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.TDigestCreate(ctx, "tdigest3").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest3", 50, 60, 70, 80, 90, 100, 110, 120, 130, 140).Err() - Expect(err).NotTo(HaveOccurred()) - - options := &redis.TDigestMergeOptions{ - Compression: 1000, - Override: false, - } - err = client.TDigestMerge(ctx, "tdigest1", options, "tdigest2", "tdigest3").Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(30))) - Expect(info.Compression).To(BeEquivalentTo(int64(1000))) - - max, err := client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(max).To(BeEquivalentTo(float64(140))) + }) + + Describe("bloom", Label("bloom"), func() { + It("should BFAdd", Label("bloom", "bfadd"), func() { + resultAdd, err := client.BFAdd(ctx, "testbf1", 1).Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeTrue()) + + resultInfo, err := client.BFInfo(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should BFCard", Label("bloom", "bfcard"), func() { + // This is a probabilistic data structure, and it's not always guaranteed that we will get back + // the exact number of inserted items, during hash collisions + // But with such a low number of items (only 3), + // the probability of a collision is very low, so we can expect to get back the exact number of items + _, err := client.BFAdd(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.BFAdd(ctx, "testbf1", "item2").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.BFAdd(ctx, "testbf1", 3).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFCard(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(int64(3))) + }) + + It("should BFExists", Label("bloom", "bfexists"), func() { + exists, err := client.BFExists(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) + + _, err = client.BFAdd(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + + exists, err = client.BFExists(ctx, "testbf1", "item1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + }) + + It("should BFInfo and BFReserve", Label("bloom", "bfinfo", "bfreserve"), func() { + err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + }) + + It("should BFInfoCapacity, BFInfoSize, BFInfoFilters, BFInfoItems, BFInfoExpansion, ", Label("bloom", "bfinfocapacity", "bfinfosize", "bfinfofilters", "bfinfoitems", "bfinfoexpansion"), func() { + err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfoCapacity(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + + result, err = client.BFInfoItems(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.ItemsInserted).To(BeEquivalentTo(int64(0))) + + result, err = client.BFInfoSize(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Size).To(BeEquivalentTo(int64(4056))) + + err = client.BFReserveExpansion(ctx, "testbf2", 0.001, 2000, 3).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.BFInfoFilters(ctx, "testbf2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Filters).To(BeEquivalentTo(int64(1))) + + result, err = client.BFInfoExpansion(ctx, "testbf2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFInsert", Label("bloom", "bfinsert"), func() { + options := &redis.BFInsertOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + NoCreate: true, + } + + _, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("ERR not found")) + + options = &redis.BFInsertOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + NoCreate: false, + } + + resultInsert, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultInsert)).To(BeEquivalentTo(1)) + + exists, err := client.BFExists(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFMAdd", Label("bloom", "bfmadd"), func() { + resultAdd, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultAdd)).To(Equal(3)) + + resultInfo, err := client.BFInfo(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(3))) + resultAdd2, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd2[0]).To(BeFalse()) + Expect(resultAdd2[1]).To(BeFalse()) + Expect(resultAdd2[2]).To(BeTrue()) + }) + + It("should BFMExists", Label("bloom", "bfmexists"), func() { + exist, err := client.BFMExists(ctx, "testbf1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(exist)).To(Equal(3)) + Expect(exist[0]).To(BeFalse()) + Expect(exist[1]).To(BeFalse()) + Expect(exist[2]).To(BeFalse()) + + _, err = client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + + exist, err = client.BFMExists(ctx, "testbf1", "item1", "item2", "item3", "item4").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(len(exist)).To(Equal(4)) + Expect(exist[0]).To(BeTrue()) + Expect(exist[1]).To(BeTrue()) + Expect(exist[2]).To(BeTrue()) + Expect(exist[3]).To(BeFalse()) + }) + + It("should BFReserveExpansion", Label("bloom", "bfreserveexpansion"), func() { + err := client.BFReserveExpansion(ctx, "testbf1", 0.001, 2000, 3).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFReserveNonScaling", Label("bloom", "bfreservenonscaling"), func() { + err := client.BFReserveNonScaling(ctx, "testbfns1", 0.001, 1000).Err() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.BFInfo(ctx, "testbfns1").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should BFScanDump and BFLoadChunk", Label("bloom", "bfscandump", "bfloadchunk"), func() { + err := client.BFReserve(ctx, "testbfsd1", 0.001, 3000).Err() + Expect(err).NotTo(HaveOccurred()) + for i := 0; i < 1000; i++ { + client.BFAdd(ctx, "testbfsd1", i) + } + infBefore := client.BFInfoSize(ctx, "testbfsd1") + fd := []redis.ScanDump{} + sd, err := client.BFScanDump(ctx, "testbfsd1", 0).Result() + for { + if sd.Iter == 0 { + break + } + Expect(err).NotTo(HaveOccurred()) + fd = append(fd, sd) + sd, err = client.BFScanDump(ctx, "testbfsd1", sd.Iter).Result() + } + client.Del(ctx, "testbfsd1") + for _, e := range fd { + client.BFLoadChunk(ctx, "testbfsd1", e.Iter, e.Data) + } + infAfter := client.BFInfoSize(ctx, "testbfsd1") + Expect(infBefore).To(BeEquivalentTo(infAfter)) + }) + + It("should BFReserveWithArgs", Label("bloom", "bfreserveargs"), func() { + options := &redis.BFReserveOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + } + err := client.BFReserveWithArgs(ctx, "testbf", options).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + }) + + Describe("cuckoo", Label("cuckoo"), func() { + It("should CFAdd", Label("cuckoo", "cfadd"), func() { + add, err := client.CFAdd(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(BeTrue()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + info, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should CFAddNX", Label("cuckoo", "cfaddnx"), func() { + add, err := client.CFAddNX(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(BeTrue()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + result, err := client.CFAddNX(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeFalse()) + + info, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should CFCount", Label("cuckoo", "cfcount"), func() { + err := client.CFAdd(ctx, "testcf1", "item1").Err() + cnt, err := client.CFCount(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cnt).To(BeEquivalentTo(int64(1))) + + err = client.CFAdd(ctx, "testcf1", "item1").Err() + Expect(err).NotTo(HaveOccurred()) + + cnt, err = client.CFCount(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cnt).To(BeEquivalentTo(int64(2))) + }) + + It("should CFDel and CFExists", Label("cuckoo", "cfdel", "cfexists"), func() { + err := client.CFAdd(ctx, "testcf1", "item1").Err() + Expect(err).NotTo(HaveOccurred()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + del, err := client.CFDel(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(del).To(BeTrue()) + + exists, err = client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) + }) + + It("should CFInfo and CFReserve", Label("cuckoo", "cfinfo", "cfreserve"), func() { + err := client.CFReserve(ctx, "testcf1", 1000).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveExpansion(ctx, "testcfe1", 1000, 1).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveBucketSize(ctx, "testcfbs1", 1000, 4).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveMaxIterations(ctx, "testcfmi1", 1000, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) + }) + + It("should CFScanDump and CFLoadChunk", Label("bloom", "cfscandump", "cfloadchunk"), func() { + err := client.CFReserve(ctx, "testcfsd1", 1000).Err() + Expect(err).NotTo(HaveOccurred()) + for i := 0; i < 1000; i++ { + Item := fmt.Sprintf("item%d", i) + client.CFAdd(ctx, "testcfsd1", Item) + } + infBefore := client.CFInfo(ctx, "testcfsd1") + fd := []redis.ScanDump{} + sd, err := client.CFScanDump(ctx, "testcfsd1", 0).Result() + for { + if sd.Iter == 0 { + break + } + Expect(err).NotTo(HaveOccurred()) + fd = append(fd, sd) + sd, err = client.CFScanDump(ctx, "testcfsd1", sd.Iter).Result() + } + client.Del(ctx, "testcfsd1") + for _, e := range fd { + client.CFLoadChunk(ctx, "testcfsd1", e.Iter, e.Data) + } + infAfter := client.CFInfo(ctx, "testcfsd1") + Expect(infBefore).To(BeEquivalentTo(infAfter)) + }) + + It("should CFInfo and CFReserveWithArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { + args := &redis.CFReserveOptions{ + Capacity: 2048, + BucketSize: 3, + MaxIterations: 15, + Expansion: 2, + } + + err := client.CFReserveWithArgs(ctx, "testcf1", args).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(result.BucketSize).To(BeEquivalentTo(int64(3))) + Expect(result.MaxIteration).To(BeEquivalentTo(int64(15))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(2))) + }) + + It("should CFInsert", Label("cuckoo", "cfinsert"), func() { + args := &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: true, + } + + result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + Expect(err).To(HaveOccurred()) + + args = &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: false, + } + + result, err = client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + }) + + It("should CFInsertNX", Label("cuckoo", "cfinsertnx"), func() { + args := &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: true, + } + + _, err := client.CFInsertNX(ctx, "testcf1", args, "item1", "item2", "item2").Result() + Expect(err).To(HaveOccurred()) + + args = &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: false, + } + + result, err := client.CFInsertNX(ctx, "testcf2", args, "item1", "item2", "item2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(1))) + Expect(result[2]).To(BeEquivalentTo(int64(0))) + }) + + It("should CFMexists", Label("cuckoo", "cfmexists"), func() { + err := client.CFInsert(ctx, "testcf1", nil, "item1", "item2", "item3").Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFMExists(ctx, "testcf1", "item1", "item2", "item3", "item4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(4)) + Expect(result[0]).To(BeTrue()) + Expect(result[1]).To(BeTrue()) + Expect(result[2]).To(BeTrue()) + Expect(result[3]).To(BeFalse()) + }) + }) + + Describe("CMS", Label("cms"), func() { + It("should CMSIncrBy", Label("cms", "cmsincrby"), func() { + err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CMSIncrBy(ctx, "testcms1", "item1", 1, "item2", 2, "item3", 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(2))) + Expect(result[2]).To(BeEquivalentTo(int64(3))) + }) + + It("should CMSInitByDim and CMSInfo", Label("cms", "cmsinitbydim", "cmsinfo"), func() { + err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.CMSInfo(ctx, "testcms1").Result() + Expect(err).NotTo(HaveOccurred()) + + Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) + Expect(info.Width).To(BeEquivalentTo(int64(5))) + Expect(info.Depth).To(BeEquivalentTo(int64(10))) + }) + + It("should CMSInitByProb", Label("cms", "cmsinitbyprob"), func() { + err := client.CMSInitByProb(ctx, "testcms1", 0.002, 0.01).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.CMSInfo(ctx, "testcms1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) + }) + + It("should CMSMerge, CMSMergeWithWeight and CMSQuery", Label("cms", "cmsmerge", "cmsquery", "NonRedisEnterprise"), func() { + err := client.CMSMerge(ctx, "destCms1", "testcms2", "testcms3").Err() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("CMS: key does not exist")) + + err = client.CMSInitByDim(ctx, "destCms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "destCms2", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms1", 2, 20).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms2", 3, 20).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() + Expect(err).To(MatchError("CMS: width/depth is not equal")) + + client.Del(ctx, "cms1", "cms2") + + err = client.CMSInitByDim(ctx, "cms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms2", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + client.CMSIncrBy(ctx, "cms1", "item1", 1, "item2", 2) + client.CMSIncrBy(ctx, "cms2", "item2", 2, "item3", 3) + + err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CMSQuery(ctx, "destCms1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(4))) + Expect(result[2]).To(BeEquivalentTo(int64(3))) + + sourceSketches := map[string]int64{ + "cms1": 1, + "cms2": 2, + } + err = client.CMSMergeWithWeight(ctx, "destCms2", sourceSketches).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.CMSQuery(ctx, "destCms2", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(6))) + Expect(result[2]).To(BeEquivalentTo(int64(6))) + }) + }) + + Describe("TopK", Label("topk"), func() { + It("should TopKReserve, TopKInfo, TopKAdd, TopKQuery, TopKCount, TopKIncrBy, TopKList, TopKListWithCount", Label("topk", "topkreserve", "topkinfo", "topkadd", "topkquery", "topkcount", "topkincrby", "topklist", "topklistwithcount"), func() { + err := client.TopKReserve(ctx, "topk1", 3).Err() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TopKInfo(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) + + resultAdd, err := client.TopKAdd(ctx, "topk1", "item1", "item2", 3, "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultAdd)).To(BeEquivalentTo(int64(4))) + + resultQuery, err := client.TopKQuery(ctx, "topk1", "item1", "item2", 4, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultQuery)).To(BeEquivalentTo(4)) + Expect(resultQuery[0]).To(BeTrue()) + Expect(resultQuery[1]).To(BeTrue()) + Expect(resultQuery[2]).To(BeFalse()) + Expect(resultQuery[3]).To(BeTrue()) + + resultCount, err := client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultCount)).To(BeEquivalentTo(3)) + Expect(resultCount[0]).To(BeEquivalentTo(int64(2))) + Expect(resultCount[1]).To(BeEquivalentTo(int64(1))) + Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) + + resultIncr, err := client.TopKIncrBy(ctx, "topk1", "item1", 5, "item2", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultIncr)).To(BeEquivalentTo(2)) + + resultCount, err = client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultCount)).To(BeEquivalentTo(3)) + Expect(resultCount[0]).To(BeEquivalentTo(int64(7))) + Expect(resultCount[1]).To(BeEquivalentTo(int64(11))) + Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) + + resultList, err := client.TopKList(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultList)).To(BeEquivalentTo(3)) + Expect(resultList).To(ContainElements("item2", "item1", "3")) + + resultListWithCount, err := client.TopKListWithCount(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultListWithCount)).To(BeEquivalentTo(3)) + Expect(resultListWithCount["3"]).To(BeEquivalentTo(int64(1))) + Expect(resultListWithCount["item1"]).To(BeEquivalentTo(int64(7))) + Expect(resultListWithCount["item2"]).To(BeEquivalentTo(int64(11))) + }) + + It("should TopKReserveWithOptions", Label("topk", "topkreservewithoptions"), func() { + err := client.TopKReserveWithOptions(ctx, "topk1", 3, 1500, 8, 0.5).Err() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TopKInfo(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) + Expect(resultInfo.Width).To(BeEquivalentTo(int64(1500))) + Expect(resultInfo.Depth).To(BeEquivalentTo(int64(8))) + Expect(resultInfo.Decay).To(BeEquivalentTo(0.5)) + }) + }) + + Describe("t-digest", Label("tdigest"), func() { + It("should TDigestAdd, TDigestCreate, TDigestInfo, TDigestByRank, TDigestByRevRank, TDigestCDF, TDigestMax, TDigestMin, TDigestQuantile, TDigestRank, TDigestRevRank, TDigestTrimmedMean, TDigestReset, ", Label("tdigest", "tdigestadd", "tdigestcreate", "tdigestinfo", "tdigestbyrank", "tdigestbyrevrank", "tdigestcdf", "tdigestmax", "tdigestmin", "tdigestquantile", "tdigestrank", "tdigestrevrank", "tdigesttrimmedmean", "tdigestreset"), func() { + err := client.TDigestCreate(ctx, "tdigest1").Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(0))) + + // Test with empty sketch + byRank, err := client.TDigestByRank(ctx, "tdigest1", 0, 1, 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRank)).To(BeEquivalentTo(4)) + + byRevRank, err := client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRevRank)).To(BeEquivalentTo(3)) + + cdf, err := client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(cdf)).To(BeEquivalentTo(3)) + + max, err := client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(max)).To(BeTrue()) + + min, err := client.TDigestMin(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(min)).To(BeTrue()) + + quantile, err := client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(quantile)).To(BeEquivalentTo(2)) + + rank, err := client.TDigestRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rank)).To(BeEquivalentTo(2)) + + revRank, err := client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(revRank)).To(BeEquivalentTo(2)) + + trimmedMean, err := client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(trimmedMean)).To(BeTrue()) + + // Add elements + err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err = client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(10))) + + byRank, err = client.TDigestByRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRank)).To(BeEquivalentTo(3)) + Expect(byRank[0]).To(BeEquivalentTo(float64(10))) + Expect(byRank[1]).To(BeEquivalentTo(float64(20))) + Expect(byRank[2]).To(BeEquivalentTo(float64(30))) + + byRevRank, err = client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRevRank)).To(BeEquivalentTo(3)) + Expect(byRevRank[0]).To(BeEquivalentTo(float64(100))) + Expect(byRevRank[1]).To(BeEquivalentTo(float64(90))) + Expect(byRevRank[2]).To(BeEquivalentTo(float64(80))) + + cdf, err = client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(cdf)).To(BeEquivalentTo(3)) + Expect(cdf[0]).To(BeEquivalentTo(0.1)) + Expect(cdf[1]).To(BeEquivalentTo(0.3)) + Expect(cdf[2]).To(BeEquivalentTo(0.65)) + + max, err = client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(max).To(BeEquivalentTo(float64(100))) + + min, err = client.TDigestMin(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(min).To(BeEquivalentTo(float64(10))) + + quantile, err = client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(quantile)).To(BeEquivalentTo(2)) + Expect(quantile[0]).To(BeEquivalentTo(float64(20))) + Expect(quantile[1]).To(BeEquivalentTo(float64(30))) + + rank, err = client.TDigestRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rank)).To(BeEquivalentTo(2)) + Expect(rank[0]).To(BeEquivalentTo(int64(0))) + Expect(rank[1]).To(BeEquivalentTo(int64(1))) + + revRank, err = client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(revRank)).To(BeEquivalentTo(2)) + Expect(revRank[0]).To(BeEquivalentTo(int64(9))) + Expect(revRank[1]).To(BeEquivalentTo(int64(8))) + + trimmedMean, err = client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(trimmedMean).To(BeEquivalentTo(float64(40))) + + reset, err := client.TDigestReset(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(reset).To(BeEquivalentTo("OK")) + }) + + It("should TDigestCreateWithCompression", Label("tdigest", "tcreatewithcompression"), func() { + err := client.TDigestCreateWithCompression(ctx, "tdigest1", 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Compression).To(BeEquivalentTo(int64(2000))) + }) + + It("should TDigestMerge", Label("tdigest", "tmerge", "NonRedisEnterprise"), func() { + err := client.TDigestCreate(ctx, "tdigest1").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.TDigestCreate(ctx, "tdigest2").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest2", 15, 25, 35, 45, 55, 65, 75, 85, 95, 105).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.TDigestCreate(ctx, "tdigest3").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest3", 50, 60, 70, 80, 90, 100, 110, 120, 130, 140).Err() + Expect(err).NotTo(HaveOccurred()) + + options := &redis.TDigestMergeOptions{ + Compression: 1000, + Override: false, + } + err = client.TDigestMerge(ctx, "tdigest1", options, "tdigest2", "tdigest3").Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(30))) + Expect(info.Compression).To(BeEquivalentTo(int64(1000))) + + max, err := client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(max).To(BeEquivalentTo(float64(140))) + }) + }) }) - }) + } }) From 92634e1c1a70b4412e18399f4fadfc004805b0cf Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:20:59 +0200 Subject: [PATCH 066/230] Add guidance on unstable RESP3 support for RediSearch commands to README (#3177) * Add UnstableResp3 to docs * Add RawVal and RawResult to wordlist * Explain more about SetVal * Add UnstableResp to wordlist --- .github/wordlist.txt | 3 +++ README.md | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index c200c60b44..1fc34f733c 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -54,6 +54,7 @@ stunnel SynDump TCP TLS +UnstableResp uri URI url @@ -62,3 +63,5 @@ RedisStack RedisGears RedisTimeseries RediSearch +RawResult +RawVal \ No newline at end of file diff --git a/README.md b/README.md index 37714a9796..e71367659d 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,21 @@ rdb := redis.NewClient(&redis.Options{ #### Unstable RESP3 Structures for RediSearch Commands When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. +To enable unstable RESP3, set the option in your client configuration: + +```go +redis.NewClient(&redis.Options{ + UnstableResp3: true, + }) +``` +**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data. + Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them: + +```go +res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult() +val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal() +``` + ## Contributing Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! From 4f48200a0a90664a3fae05522c1e1d02ecdc4217 Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Wed, 20 Nov 2024 03:38:06 -0800 Subject: [PATCH 067/230] Eliminate redundant dial mutex causing unbounded connection queue contention (#3088) * Eliminate redundant dial mutex causing unbounded connection queue contention * Dialer connection timeouts unit test --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- redis.go | 2 -- redis_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/redis.go b/redis.go index c8b5008090..2f576bdbe3 100644 --- a/redis.go +++ b/redis.go @@ -176,8 +176,6 @@ func (hs *hooksMixin) withProcessPipelineHook( } func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) { - hs.hooksMu.Lock() - defer hs.hooksMu.Unlock() return hs.current.dial(ctx, network, addr) } diff --git a/redis_test.go b/redis_test.go index ef21254522..b5cf2570f1 100644 --- a/redis_test.go +++ b/redis_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net" + "sync" "testing" "time" @@ -633,3 +634,67 @@ var _ = Describe("Hook with MinIdleConns", func() { })) }) }) + +var _ = Describe("Dialer connection timeouts", func() { + var client *redis.Client + + const dialSimulatedDelay = 1 * time.Second + + BeforeEach(func() { + options := redisOptions() + options.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { + // Simulated slow dialer. + // Note that the following sleep is deliberately not context-aware. + time.Sleep(dialSimulatedDelay) + return net.Dial("tcp", options.Addr) + } + options.MinIdleConns = 1 + client = redis.NewClient(options) + }) + + AfterEach(func() { + err := client.Close() + Expect(err).NotTo(HaveOccurred()) + }) + + It("does not contend on connection dial for concurrent commands", func() { + var wg sync.WaitGroup + + const concurrency = 10 + + durations := make(chan time.Duration, concurrency) + errs := make(chan error, concurrency) + + start := time.Now() + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + + start := time.Now() + err := client.Ping(ctx).Err() + durations <- time.Since(start) + errs <- err + }() + } + + wg.Wait() + close(durations) + close(errs) + + // All commands should eventually succeed, after acquiring a connection. + for err := range errs { + Expect(err).NotTo(HaveOccurred()) + } + + // Each individual command should complete within the simulated dial duration bound. + for duration := range durations { + Expect(duration).To(BeNumerically("<", 2*dialSimulatedDelay)) + } + + // Due to concurrent execution, the entire test suite should also complete within + // the same dial duration bound applied for individual commands. + Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay)) + }) +}) From 7cf9f53eb2a76a0f7b16a0947079d182b082bc52 Mon Sep 17 00:00:00 2001 From: Justin <8886628+justinmir@users.noreply.github.com> Date: Wed, 20 Nov 2024 06:36:39 -0600 Subject: [PATCH 068/230] Only check latencies once every 10 seconds with `routeByLatency` (#2795) * Only check latencies once every 10 seconds with `routeByLatency` `routeByLatency` currently checks latencies any time a server returns a MOVED or READONLY reply. When a shard is down, the ClusterClient chooses to issue the request to a random server, which returns a MOVED reply. This causes a state refresh and a latency update on all servers. This can lead to significant ping load to clusters with a large number of clients. This introduces logic to ping only once every 10 seconds, only performing a latency update on a node during the `GC` function if the latency was set later than 10 seconds ago. Fixes https://github.com/redis/go-redis/issues/2782 * use UnixNano instead of Unix for better precision --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- osscluster.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osscluster.go b/osscluster.go index ce258ff363..72e922a80d 100644 --- a/osscluster.go +++ b/osscluster.go @@ -21,6 +21,10 @@ import ( "github.com/redis/go-redis/v9/internal/rand" ) +const ( + minLatencyMeasurementInterval = 10 * time.Second +) + var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes") // ClusterOptions are used to configure a cluster client and should be @@ -316,6 +320,10 @@ type clusterNode struct { latency uint32 // atomic generation uint32 // atomic failing uint32 // atomic + + // last time the latency measurement was performed for the node, stored in nanoseconds + // from epoch + lastLatencyMeasurement int64 // atomic } func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode { @@ -368,6 +376,7 @@ func (n *clusterNode) updateLatency() { latency = float64(dur) / float64(successes) } atomic.StoreUint32(&n.latency, uint32(latency+0.5)) + n.SetLastLatencyMeasurement(time.Now()) } func (n *clusterNode) Latency() time.Duration { @@ -397,6 +406,10 @@ func (n *clusterNode) Generation() uint32 { return atomic.LoadUint32(&n.generation) } +func (n *clusterNode) LastLatencyMeasurement() int64 { + return atomic.LoadInt64(&n.lastLatencyMeasurement) +} + func (n *clusterNode) SetGeneration(gen uint32) { for { v := atomic.LoadUint32(&n.generation) @@ -406,6 +419,15 @@ func (n *clusterNode) SetGeneration(gen uint32) { } } +func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) { + for { + v := atomic.LoadInt64(&n.lastLatencyMeasurement) + if t.UnixNano() < v || atomic.CompareAndSwapInt64(&n.lastLatencyMeasurement, v, t.UnixNano()) { + break + } + } +} + //------------------------------------------------------------------------------ type clusterNodes struct { @@ -493,10 +515,11 @@ func (c *clusterNodes) GC(generation uint32) { c.mu.Lock() c.activeAddrs = c.activeAddrs[:0] + now := time.Now() for addr, node := range c.nodes { if node.Generation() >= generation { c.activeAddrs = append(c.activeAddrs, addr) - if c.opt.RouteByLatency { + if c.opt.RouteByLatency && node.LastLatencyMeasurement() < now.Add(-minLatencyMeasurementInterval).UnixNano() { go node.updateLatency() } continue From f2fa1ce4773e04a98a8c923e0ca69c2dff11c62f Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Thu, 21 Nov 2024 04:38:11 -0800 Subject: [PATCH 069/230] Recognize byte slice for key argument in cluster client hash slot computation (#3049) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- command.go | 2 ++ osscluster_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/command.go b/command.go index 7ea7862d5f..3cb9538a54 100644 --- a/command.go +++ b/command.go @@ -167,6 +167,8 @@ func (cmd *baseCmd) stringArg(pos int) string { switch v := arg.(type) { case string: return v + case []byte: + return string(v) default: // TODO: consider using appendArg return fmt.Sprint(v) diff --git a/osscluster_test.go b/osscluster_test.go index f7bd1683f8..9c3eaba353 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -653,6 +653,32 @@ var _ = Describe("ClusterClient", func() { Expect(client.Close()).NotTo(HaveOccurred()) }) + It("determines hash slots correctly for generic commands", func() { + opt := redisClusterOptions() + opt.MaxRedirects = -1 + client := cluster.newClusterClient(ctx, opt) + + err := client.Do(ctx, "GET", "A").Err() + Expect(err).To(Equal(redis.Nil)) + + err = client.Do(ctx, []byte("GET"), []byte("A")).Err() + Expect(err).To(Equal(redis.Nil)) + + Eventually(func() error { + return client.SwapNodes(ctx, "A") + }, 30*time.Second).ShouldNot(HaveOccurred()) + + err = client.Do(ctx, "GET", "A").Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MOVED")) + + err = client.Do(ctx, []byte("GET"), []byte("A")).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MOVED")) + + Expect(client.Close()).NotTo(HaveOccurred()) + }) + It("follows node redirection immediately", func() { // Configure retry backoffs far in excess of the expected duration of redirection opt := redisClusterOptions() From dec9e1e73fd67310603e92f1cdc4f017ea6b13df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:38:38 +0200 Subject: [PATCH 070/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3188) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.40.0 to 0.45.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.40.0...0.45.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index cc6d828c93..977f8c5c11 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.40.0 + uses: rojopolis/spellcheck-github-actions@0.45.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From cfbb6f5a03e231fc4644b0edb75ffd859b81259b Mon Sep 17 00:00:00 2001 From: Cgol9 Date: Thu, 5 Dec 2024 01:10:04 -0700 Subject: [PATCH 071/230] SortByWithCount FTSearchOptions fix (#3201) * SortByWithCount FTSearchOptions fix * FTSearch test fix * Another FTSearch test fix * Another FTSearch test fix --------- Co-authored-by: Christopher Golling --- search_commands.go | 2 +- search_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/search_commands.go b/search_commands.go index e4df0b6fc5..ede084e4e1 100644 --- a/search_commands.go +++ b/search_commands.go @@ -1775,7 +1775,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { } } if options.SortByWithCount { - queryArgs = append(queryArgs, "WITHCOUT") + queryArgs = append(queryArgs, "WITHCOUNT") } } if options.LimitOffset >= 0 && options.Limit > 0 { diff --git a/search_test.go b/search_test.go index 48b9aa39bc..e267c8ae86 100644 --- a/search_test.go +++ b/search_test.go @@ -125,6 +125,10 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2")) Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3")) + res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(BeEquivalentTo(int64(0))) + }) It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { From cc152fabc5a54ee60b8728f69438f45e9be76268 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:11:12 +0200 Subject: [PATCH 072/230] chore(deps): bump codecov/codecov-action from 4 to 5 (#3196) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5007423a4f..c1d04b8206 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: run: make test - name: Upload to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage.txt token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file From 450b2687853162ffdcc7a27920a9a0cd17024e79 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:04:39 +0200 Subject: [PATCH 073/230] Test against Redis CE (#3191) * Create workflow that tests go-redis against docker * Add docker compose file * Add docker compose file * Change command in docker compose * Load modules locally * test varios redis versions * add env var to test-redis-enterprise action * cleaning code * cleaning code --- .github/workflows/build.yml | 45 ++++++++++++++++++++- .github/workflows/test-redis-enterprise.yml | 1 + docker-compose.yml | 21 ++++++++++ main_test.go | 5 ++- 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1d04b8206..7578e962e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,4 +42,47 @@ jobs: uses: codecov/codecov-action@v5 with: files: coverage.txt - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + + test-redis-ce: + name: test-redis-ce + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + redis_version: + - "8.0-M01" + - "7.4.1" + - "7.2.6" + - "6.2.16" + go-version: + - "1.19.x" + - "1.20.x" + - "1.21.x" + + steps: + - name: Set up ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Docker Compose environment + - name: Set up Docker Compose environment + run: | + docker compose --profile all up -d + + - name: Run tests + env: + USE_CONTAINERIZED_REDIS: "true" + RE_CLUSTER: "true" + run: | + go test \ + --ginkgo.skip-file="ring_test.go" \ + --ginkgo.skip-file="sentinel_test.go" \ + --ginkgo.skip-file="osscluster_test.go" \ + --ginkgo.skip-file="pubsub_test.go" \ + --ginkgo.skip-file="gears_commands_test.go" \ + --ginkgo.label-filter='!NonRedisEnterprise' diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 940f0eae79..1cb36b8d29 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -47,6 +47,7 @@ jobs: - name: Test env: RE_CLUSTER: "1" + USE_CONTAINERIZED_REDIS: "1" run: | go test \ --ginkgo.skip-file="ring_test.go" \ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..a641e4d3b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +--- + +services: + + redis-stanalone: + image: redislabs/client-libs-test:8.0-M02 + container_name: redis-standalone + environment: + - REDIS_CLUSTER=no + - PORT=6379 + - TLS_PORT=6666 + command: --loadmodule /usr/local/lib/redis/modules/redisbloom.so --loadmodule /usr/local/lib/redis/modules/redisearch.so --loadmodule /usr/local/lib/redis/modules/redistimeseries.so --loadmodule /usr/local/lib/redis/modules/rejson.so + ports: + - 6379:6379 + - 6380:6379 + - 6666:6666 # TLS port + volumes: + - "./dockers/redis-standalone:/redis/work" + profiles: + - standalone + - all diff --git a/main_test.go b/main_test.go index 19e944446e..44f8e6829d 100644 --- a/main_test.go +++ b/main_test.go @@ -66,6 +66,7 @@ var cluster = &clusterScenario{ } var RECluster = false +var USE_CONTAINERIZED_REDIS = false func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -82,8 +83,8 @@ var _ = BeforeSuite(func() { } var err error RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) - - if !RECluster { + USE_CONTAINERIZED_REDIS, _ = strconv.ParseBool(os.Getenv("USE_CONTAINERIZED_REDIS")) + if !RECluster || !USE_CONTAINERIZED_REDIS { redisMain, err = startRedis(redisPort) Expect(err).NotTo(HaveOccurred()) From 085338ffb813c20634169f09f0050fb4f430ec20 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:24:47 +0200 Subject: [PATCH 074/230] Fix Redis CE tests (#3233) * Fix Redis CE tests * Remove manually modules installation --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a641e4d3b0..f5ccb8f421 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 - command: --loadmodule /usr/local/lib/redis/modules/redisbloom.so --loadmodule /usr/local/lib/redis/modules/redisearch.so --loadmodule /usr/local/lib/redis/modules/redistimeseries.so --loadmodule /usr/local/lib/redis/modules/rejson.so + ports: - 6379:6379 - 6380:6379 From b167cb19fbad9c02835754e2ae1a8aa3c651edcc Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:29:51 +0000 Subject: [PATCH 075/230] DOC-4560 pipelines/transactions example (#3202) * DOC-4560 basic transaction example * DOC-4560 added pipe/transaction examples --- doctests/pipe_trans_example_test.go | 180 ++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 doctests/pipe_trans_example_test.go diff --git a/doctests/pipe_trans_example_test.go b/doctests/pipe_trans_example_test.go new file mode 100644 index 0000000000..ea1dd5b482 --- /dev/null +++ b/doctests/pipe_trans_example_test.go @@ -0,0 +1,180 @@ +// EXAMPLE: pipe_trans_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_transactions() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + // REMOVE_START + for i := 0; i < 5; i++ { + rdb.Del(ctx, fmt.Sprintf("seat:%d", i)) + } + + rdb.Del(ctx, "counter:1", "counter:2", "counter:3", "shellpath") + // REMOVE_END + + // STEP_START basic_pipe + pipe := rdb.Pipeline() + + for i := 0; i < 5; i++ { + pipe.Set(ctx, fmt.Sprintf("seat:%v", i), fmt.Sprintf("#%v", i), 0) + } + + cmds, err := pipe.Exec(ctx) + + if err != nil { + panic(err) + } + + for _, c := range cmds { + fmt.Printf("%v;", c.(*redis.StatusCmd).Val()) + } + + fmt.Println("") + // >>> OK;OK;OK;OK;OK; + + pipe = rdb.Pipeline() + + get0Result := pipe.Get(ctx, "seat:0") + get3Result := pipe.Get(ctx, "seat:3") + get4Result := pipe.Get(ctx, "seat:4") + + cmds, err = pipe.Exec(ctx) + + // The results are available only after the pipeline + // has finished executing. + fmt.Println(get0Result.Val()) // >>> #0 + fmt.Println(get3Result.Val()) // >>> #3 + fmt.Println(get4Result.Val()) // >>> #4 + // STEP_END + + // STEP_START basic_pipe_pipelined + var pd0Result *redis.StatusCmd + var pd3Result *redis.StatusCmd + var pd4Result *redis.StatusCmd + + cmds, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { + pd0Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:0")) + pd3Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:3")) + pd4Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:4")) + return nil + }) + + if err != nil { + panic(err) + } + + // The results are available only after the pipeline + // has finished executing. + fmt.Println(pd0Result.Val()) // >>> #0 + fmt.Println(pd3Result.Val()) // >>> #3 + fmt.Println(pd4Result.Val()) // >>> #4 + // STEP_END + + // STEP_START basic_trans + trans := rdb.TxPipeline() + + trans.IncrBy(ctx, "counter:1", 1) + trans.IncrBy(ctx, "counter:2", 2) + trans.IncrBy(ctx, "counter:3", 3) + + cmds, err = trans.Exec(ctx) + + for _, c := range cmds { + fmt.Println(c.(*redis.IntCmd).Val()) + } + // >>> 1 + // >>> 2 + // >>> 3 + // STEP_END + + // STEP_START basic_trans_txpipelined + var tx1Result *redis.IntCmd + var tx2Result *redis.IntCmd + var tx3Result *redis.IntCmd + + cmds, err = rdb.TxPipelined(ctx, func(trans redis.Pipeliner) error { + tx1Result = trans.IncrBy(ctx, "counter:1", 1) + tx2Result = trans.IncrBy(ctx, "counter:2", 2) + tx3Result = trans.IncrBy(ctx, "counter:3", 3) + return nil + }) + + if err != nil { + panic(err) + } + + fmt.Println(tx1Result.Val()) // >>> 2 + fmt.Println(tx2Result.Val()) // >>> 4 + fmt.Println(tx3Result.Val()) // >>> 6 + // STEP_END + + // STEP_START trans_watch + // Set initial value of `shellpath`. + rdb.Set(ctx, "shellpath", "/usr/syscmds/", 0) + + const maxRetries = 1000 + + // Retry if the key has been changed. + for i := 0; i < maxRetries; i++ { + err := rdb.Watch(ctx, + func(tx *redis.Tx) error { + currentPath, err := rdb.Get(ctx, "shellpath").Result() + newPath := currentPath + ":/usr/mycmds/" + + _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, "shellpath", newPath, 0) + return nil + }) + + return err + }, + "shellpath", + ) + + if err == nil { + // Success. + break + } else if err == redis.TxFailedErr { + // Optimistic lock lost. Retry the transaction. + continue + } else { + // Panic for any other error. + panic(err) + } + } + + fmt.Println(rdb.Get(ctx, "shellpath").Val()) + // >>> /usr/syscmds/:/usr/mycmds/ + // STEP_END + + // Output: + // OK;OK;OK;OK;OK; + // #0 + // #3 + // #4 + // #0 + // #3 + // #4 + // 1 + // 2 + // 3 + // 2 + // 4 + // 6 + // /usr/syscmds/:/usr/mycmds/ +} From e6e323fe1a33d73e388a44fe38d8ea95acf98ab2 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:02:55 +0000 Subject: [PATCH 076/230] DOC-4449 hash command examples (#3229) * DOC-4450 added hgetall and hvals doc examples * DOC-4449 added hgetall and hvals doc examples * DOC-4449 rewrote to avoid Collect and Keys functions (not available in test version of Go) * DOC-4449 replaced slices.Sort function with older alternative * DOC-4449 removed another instance of slices.Sort * DOC-4449 fixed bugs in tests * DOC-4449 try sort.Strings() for sorting key lists --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/cmds_hash_test.go | 114 ++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/doctests/cmds_hash_test.go b/doctests/cmds_hash_test.go index f9630a9dee..52ade74e98 100644 --- a/doctests/cmds_hash_test.go +++ b/doctests/cmds_hash_test.go @@ -5,6 +5,7 @@ package example_commands_test import ( "context" "fmt" + "sort" "github.com/redis/go-redis/v9" ) @@ -74,8 +75,20 @@ func ExampleClient_hset() { panic(err) } - fmt.Println(res6) - // >>> map[field1:Hello field2:Hi field3:World] + keys := make([]string, 0, len(res6)) + + for key, _ := range res6 { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + fmt.Printf("Key: %v, value: %v\n", key, res6[key]) + } + // >>> Key: field1, value: Hello + // >>> Key: field2, value: Hi + // >>> Key: field3, value: World // STEP_END // Output: @@ -84,7 +97,9 @@ func ExampleClient_hset() { // 2 // Hi // World - // map[field1:Hello field2:Hi field3:World] + // Key: field1, value: Hello + // Key: field2, value: Hi + // Key: field3, value: World } func ExampleClient_hget() { @@ -131,3 +146,96 @@ func ExampleClient_hget() { // foo // redis: nil } + +func ExampleClient_hgetall() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hgetall + hGetAllResult1, err := rdb.HSet(ctx, "myhash", + "field1", "Hello", + "field2", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(hGetAllResult1) // >>> 2 + + hGetAllResult2, err := rdb.HGetAll(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + keys := make([]string, 0, len(hGetAllResult2)) + + for key, _ := range hGetAllResult2 { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + fmt.Printf("Key: %v, value: %v\n", key, hGetAllResult2[key]) + } + // >>> Key: field1, value: Hello + // >>> Key: field2, value: World + // STEP_END + + // Output: + // 2 + // Key: field1, value: Hello + // Key: field2, value: World +} + +func ExampleClient_hvals() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hvals + hValsResult1, err := rdb.HSet(ctx, "myhash", + "field1", "Hello", + "field2", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(hValsResult1) // >>> 2 + + hValsResult2, err := rdb.HVals(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + sort.Strings(hValsResult2) + + fmt.Println(hValsResult2) // >>> [Hello World] + // STEP_END + + // Output: + // 2 + // [Hello World] +} From 8863e40aea66cb4e8699e099c318b3b1d91b0c56 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 20 Jan 2025 11:32:10 +0200 Subject: [PATCH 077/230] Order slices of strings to be sure what the output of Println in doctests will be. (#3241) * Sort the slices of strings in doctest to make the output deterministic * fix wording --- doctests/sets_example_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go index 7446a2789d..2d6504e2b1 100644 --- a/doctests/sets_example_test.go +++ b/doctests/sets_example_test.go @@ -5,6 +5,7 @@ package example_commands_test import ( "context" "fmt" + "sort" "github.com/redis/go-redis/v9" ) @@ -215,6 +216,9 @@ func ExampleClient_saddsmembers() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res10) + fmt.Println(res10) // >>> [bike:1 bike:2 bike:3] // STEP_END @@ -294,6 +298,10 @@ func ExampleClient_sdiff() { panic(err) } + + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res13) + fmt.Println(res13) // >>> [bike:2 bike:3] // STEP_END @@ -349,6 +357,9 @@ func ExampleClient_multisets() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res15) + fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4] res16, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result() @@ -373,6 +384,9 @@ func ExampleClient_multisets() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res18) + fmt.Println(res18) // >>> [bike:2 bike:3] // STEP_END From 7f5c356371a93d88f1559e9b64231b44e3b15514 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 23 Jan 2025 14:47:28 +0200 Subject: [PATCH 078/230] fix(command): add missing `io-thread` key in `client info` (#3244) * Add 8.0m3 image in docker compose * Add new key `io-thread` in client info Redis 8.0 introduces new key `io-thread` in the response for client info. The key needs to be parsed. If an unknown key is observed, the client will return an error. * improve readibility * Revert "Add 8.0m3 image in docker compose" This reverts commit 787c41f42917fb7d3ca3471d9941304695a9b3c8. * add dockers directory to gitignore --- .gitignore | 3 ++- command.go | 3 +++ main_test.go | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6f868895ba..7507584f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ +dockers/ *.rdb testdata/* .idea/ .DS_Store *.tar.gz -*.dic \ No newline at end of file +*.dic diff --git a/command.go b/command.go index 3cb9538a54..f5aad91494 100644 --- a/command.go +++ b/command.go @@ -5114,6 +5114,7 @@ type ClientInfo struct { OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full) OutputMemory int // omem, output buffer memory usage TotalMemory int // tot-mem, total memory consumed by this client in its various buffers + IoThread int // io-thread id Events string // file descriptor events (see below) LastCmd string // cmd, last command played User string // the authenticated username of the client @@ -5292,6 +5293,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.LibName = val case "lib-ver": info.LibVer = val + case "io-thread": + info.IoThread, err = strconv.Atoi(val) default: return nil, fmt.Errorf("redis: unexpected client info key(%s)", key) } diff --git a/main_test.go b/main_test.go index 44f8e6829d..9f99b5c911 100644 --- a/main_test.go +++ b/main_test.go @@ -118,7 +118,8 @@ var _ = BeforeSuite(func() { sentinelSlave2Port, "--slaveof", "127.0.0.1", sentinelMasterPort) Expect(err).NotTo(HaveOccurred()) - Expect(startCluster(ctx, cluster)).NotTo(HaveOccurred()) + err = startCluster(ctx, cluster) + Expect(err).NotTo(HaveOccurred()) } else { redisPort = rediStackPort redisAddr = rediStackAddr From 7fd6c5be59443ec833aa755a965aa772a79aaed5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:56:20 +0200 Subject: [PATCH 079/230] chore(deps): bump golang.org/x/net in /example/otel (#3243) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.33.0. - [Commits](https://github.com/golang/net/compare/v0.23.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- example/otel/go.mod | 6 +++--- example/otel/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/otel/go.mod b/example/otel/go.mod index 4d97da4d1e..3f1d858e17 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -34,9 +34,9 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index 5fb4c4588f..e85481dbeb 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -46,12 +46,12 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= From e45406f07e819f5b528b30b79de2b17ebdbe7be1 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:57:14 +0000 Subject: [PATCH 080/230] DOC-4444 server management command examples (#3235) --- doctests/cmds_servermgmt_test.go | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 doctests/cmds_servermgmt_test.go diff --git a/doctests/cmds_servermgmt_test.go b/doctests/cmds_servermgmt_test.go new file mode 100644 index 0000000000..8114abc1b9 --- /dev/null +++ b/doctests/cmds_servermgmt_test.go @@ -0,0 +1,74 @@ +// EXAMPLE: cmds_servermgmt +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cmd_flushall() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // STEP_START flushall + // REMOVE_START + rdb.Set(ctx, "testkey1", "1", 0) + rdb.Set(ctx, "testkey2", "2", 0) + rdb.Set(ctx, "testkey3", "3", 0) + // REMOVE_END + flushAllResult1, err := rdb.FlushAll(ctx).Result() + + if err != nil { + panic(err) + } + + fmt.Println(flushAllResult1) // >>> OK + + flushAllResult2, err := rdb.Keys(ctx, "*").Result() + + if err != nil { + panic(err) + } + + fmt.Println(flushAllResult2) // >>> [] + // STEP_END + + // Output: + // OK + // [] +} + +func ExampleClient_cmd_info() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // STEP_START info + infoResult, err := rdb.Info(ctx).Result() + + if err != nil { + panic(err) + } + + // Check the first 8 characters (the full info string contains + // much more text than this). + fmt.Println(infoResult[:8]) // >>> # Server + // STEP_END + + // Output: + // # Server +} From cab668d936662056de4445d32225c805e3279763 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 31 Jan 2025 16:14:11 +0200 Subject: [PATCH 081/230] fix(tests): enable testing with Redis CE 8.0-M4 in CI (#3247) * introduce github workflow for ci similar to the one in redis-py use prerelease for 8.0-M4 * Enable osscluster tests in CI * Add redis major version env Enable filtering test per redis major version Fix test for FT.SEARCH WITHSCORE, the default scorer has changed. fix Makefile syntax remove filter from github action fix makefile use the container name in Makefile * remove 1.20 from doctests * self review, cleanup, add comments * add comments, reorder prints, add default value for REDIS_MAJOR_VERSION --- .github/actions/run-tests/action.yml | 62 +++++++ .github/workflows/build.yml | 51 ++---- .github/workflows/doctests.yaml | 2 +- .github/workflows/test-redis-enterprise.yml | 6 +- .gitignore | 1 - Makefile | 5 +- bench_test.go | 2 +- commands_test.go | 1 - docker-compose.yml | 131 +++++++++++++- dockers/.gitignore | 1 + dockers/sentinel.conf | 5 + main_test.go | 52 ++++-- monitor_test.go | 10 +- osscluster_test.go | 188 ++++++++++++-------- search_commands.go | 39 ++-- search_test.go | 49 +++++ 16 files changed, 445 insertions(+), 160 deletions(-) create mode 100644 .github/actions/run-tests/action.yml create mode 100644 dockers/.gitignore create mode 100644 dockers/sentinel.conf diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 0000000000..95709b5df8 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,62 @@ +name: 'Run go-redis tests' +description: 'Runs go-redis tests against different Redis versions and configurations' +inputs: + go-version: + description: 'Go version to use for running tests' + default: '1.23' + redis-version: + description: 'Redis version to test against' + required: true +runs: + using: "composite" + steps: + - name: Set up ${{ inputs.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: Setup Test environment + env: + REDIS_VERSION: ${{ inputs.redis-version }} + CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}" + run: | + set -e + redis_major_version=$(echo "$REDIS_VERSION" | grep -oP '^\d+') + if (( redis_major_version < 8 )); then + echo "Using redis-stack for module tests" + else + echo "Using redis CE for module tests" + fi + + # Mapping of redis version to redis testing containers + declare -A redis_version_mapping=( + ["8.0-M03"]="8.0-M04-pre" + ["7.4.2"]="rs-7.4.0-v2" + ["7.2.7"]="rs-7.2.0-v14" + ) + + if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then + echo "REDIS_MAJOR_VERSION=${redis_major_version}" >> $GITHUB_ENV + echo "REDIS_IMAGE=redis:${{ inputs.redis-version }}" >> $GITHUB_ENV + echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV + else + echo "Version not found in the mapping." + exit 1 + fi + sleep 10 # time to settle + shell: bash + - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }} + run: docker compose --profile all up -d + shell: bash + - name: Run tests + env: + RCE_DOCKER: "true" + RE_CLUSTER: "false" + run: | + go test \ + --ginkgo.skip-file="ring_test.go" \ + --ginkgo.skip-file="sentinel_test.go" \ + --ginkgo.skip-file="pubsub_test.go" \ + --ginkgo.skip-file="gears_commands_test.go" \ + --ginkgo.label-filter="!NonRedisEnterprise" + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7578e962e3..5852fcde43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,15 +16,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.19.x, 1.20.x, 1.21.x] - - services: - redis: - image: redis/redis-stack-server:latest - options: >- - --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - ports: - - 6379:6379 + go-version: [1.21.x, 1.22.x, 1.23.x] steps: - name: Set up ${{ matrix.go-version }} @@ -50,39 +42,22 @@ jobs: strategy: fail-fast: false matrix: - redis_version: - - "8.0-M01" - - "7.4.1" - - "7.2.6" - - "6.2.16" + redis-version: + - "8.0-M03" # 8.0 milestone 4 + - "7.4.2" # should use redis stack 7.4 + - "7.2.7" # should redis stack 7.2 go-version: - - "1.19.x" - - "1.20.x" - - "1.21.x" + - "1.22.x" + - "1.23.x" steps: - - name: Set up ${{ matrix.go-version }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - - # Set up Docker Compose environment - - name: Set up Docker Compose environment - run: | - docker compose --profile all up -d - + - name: Run tests - env: - USE_CONTAINERIZED_REDIS: "true" - RE_CLUSTER: "true" - run: | - go test \ - --ginkgo.skip-file="ring_test.go" \ - --ginkgo.skip-file="sentinel_test.go" \ - --ginkgo.skip-file="osscluster_test.go" \ - --ginkgo.skip-file="pubsub_test.go" \ - --ginkgo.skip-file="gears_commands_test.go" \ - --ginkgo.label-filter='!NonRedisEnterprise' + uses: ./.github/actions/run-tests + with: + go-version: ${{matrix.go-version}} + redis-version: ${{ matrix.redis-version }} + diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index 6e49e64773..b04f3140b9 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.18", "1.19", "1.20", "1.21" ] + go-version: [ "1.21", "1.22", "1.23" ] steps: - name: Set up ${{ matrix.go-version }} diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 1cb36b8d29..10c27198a9 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.21.x] + go-version: [1.23.x] re-build: ["7.4.2-54"] steps: @@ -46,8 +46,8 @@ jobs: - name: Test env: - RE_CLUSTER: "1" - USE_CONTAINERIZED_REDIS: "1" + RE_CLUSTER: true + REDIS_MAJOR_VERSION: 7 run: | go test \ --ginkgo.skip-file="ring_test.go" \ diff --git a/.gitignore b/.gitignore index 7507584f0f..63b21b0b48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -dockers/ *.rdb testdata/* .idea/ diff --git a/Makefile b/Makefile index 1a6bd17862..360505ba5a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) +export REDIS_MAJOR_VERSION := 7 test: testdeps + docker start go-redis-redis-stack || docker run -d --name go-redis-redis-stack -p 6379:6379 -e REDIS_ARGS="--enable-debug-command yes --enable-module-command yes" redis/redis-stack-server:latest $(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2)) set -e; for dir in $(GO_MOD_DIRS); do \ if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \ @@ -19,6 +21,7 @@ test: testdeps done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet + docker stop go-redis-redis-stack testdeps: testdata/redis/src/redis-server @@ -32,7 +35,7 @@ build: testdata/redis: mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.4-rc2.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- https://download.redis.io/releases/redis-7.4.2.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis cd $< && make all diff --git a/bench_test.go b/bench_test.go index 8e23303f1b..bb84c4156d 100644 --- a/bench_test.go +++ b/bench_test.go @@ -277,7 +277,7 @@ func BenchmarkXRead(b *testing.B) { func newClusterScenario() *clusterScenario { return &clusterScenario{ - ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"}, + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, nodeIDs: make([]string, 6), processes: make(map[string]*redisProcess, 6), clients: make(map[string]*redis.Client, 6), diff --git a/commands_test.go b/commands_test.go index 9554bf9a9f..901e96e35e 100644 --- a/commands_test.go +++ b/commands_test.go @@ -441,7 +441,6 @@ var _ = Describe("Commands", func() { It("should Command", Label("NonRedisEnterprise"), func() { cmds, err := client.Command(ctx).Result() Expect(err).NotTo(HaveOccurred()) - Expect(len(cmds)).To(BeNumerically("~", 240, 25)) cmd := cmds["mget"] Expect(cmd.Name).To(Equal("mget")) diff --git a/docker-compose.yml b/docker-compose.yml index f5ccb8f421..fecd14feff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,140 @@ --- services: - - redis-stanalone: - image: redislabs/client-libs-test:8.0-M02 + redis: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} container_name: redis-standalone environment: + - TLS_ENABLED=yes - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 - + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} ports: - 6379:6379 - - 6380:6379 - 6666:6666 # TLS port volumes: - - "./dockers/redis-standalone:/redis/work" + - "./dockers/standalone:/redis/work" profiles: - standalone + - sentinel + - all-stack + - all + + cluster: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-cluster + environment: + - NODES=6 + - PORT=16600 + command: "--cluster-enabled yes" + ports: + - "16600-16605:16600-16605" + volumes: + - "./dockers/cluster:/redis/work" + profiles: + - cluster + - all-stack + - all + + sentinel: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26379" + ports: + - 26379:26379 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack - all + + sentinel2: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel2 + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26380" + ports: + - 26380:26380 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack + - all + + sentinel3: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel3 + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26381" + ports: + - 26381:26381 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack + - all + + redisRing1: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-1 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6390 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6390:6390 + volumes: + - "./dockers/ring1:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all + + redisRing2: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-2 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6391 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6391:6391 + volumes: + - "./dockers/ring2:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all + + redisRing3: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-3 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6392 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6392:6392 + volumes: + - "./dockers/ring3:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all \ No newline at end of file diff --git a/dockers/.gitignore b/dockers/.gitignore new file mode 100644 index 0000000000..355164c126 --- /dev/null +++ b/dockers/.gitignore @@ -0,0 +1 @@ +*/ diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf new file mode 100644 index 0000000000..7d85e430a8 --- /dev/null +++ b/dockers/sentinel.conf @@ -0,0 +1,5 @@ +sentinel resolve-hostnames yes +sentinel monitor go-redis-test redis 6379 2 +sentinel down-after-milliseconds go-redis-test 5000 +sentinel failover-timeout go-redis-test 60000 +sentinel parallel-syncs go-redis-test 1 diff --git a/main_test.go b/main_test.go index 9f99b5c911..6b3b563a02 100644 --- a/main_test.go +++ b/main_test.go @@ -13,7 +13,6 @@ import ( . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" - "github.com/redis/go-redis/v9" ) @@ -28,7 +27,7 @@ const ( ) const ( - sentinelName = "mymaster" + sentinelName = "go-redis-test" sentinelMasterPort = "9123" sentinelSlave1Port = "9124" sentinelSlave2Port = "9125" @@ -43,8 +42,8 @@ var ( ) var ( - rediStackPort = "6379" - rediStackAddr = ":" + rediStackPort + redisStackPort = "6379" + redisStackAddr = ":" + redisStackPort ) var ( @@ -59,14 +58,22 @@ var ( ) var cluster = &clusterScenario{ - ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"}, + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, nodeIDs: make([]string, 6), processes: make(map[string]*redisProcess, 6), clients: make(map[string]*redis.Client, 6), } +// Redis Software Cluster var RECluster = false -var USE_CONTAINERIZED_REDIS = false + +// Redis Community Edition Docker +var RCEDocker = false + +// Notes the major version of redis we are executing tests. +// This can be used before we change the bsm fork of ginkgo for one, +// which have support for label sets, so we can filter tests per redis major version. +var REDIS_MAJOR_VERSION = 7 func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -83,8 +90,19 @@ var _ = BeforeSuite(func() { } var err error RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) - USE_CONTAINERIZED_REDIS, _ = strconv.ParseBool(os.Getenv("USE_CONTAINERIZED_REDIS")) - if !RECluster || !USE_CONTAINERIZED_REDIS { + RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) + + REDIS_MAJOR_VERSION, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + if REDIS_MAJOR_VERSION == 0 { + REDIS_MAJOR_VERSION = 7 + } + Expect(REDIS_MAJOR_VERSION).To(BeNumerically(">=", 6)) + Expect(REDIS_MAJOR_VERSION).To(BeNumerically("<=", 8)) + + fmt.Printf("RECluster: %v\n", RECluster) + fmt.Printf("RCEDocker: %v\n", RCEDocker) + fmt.Printf("REDIS_MAJOR_VERSION: %v\n", REDIS_MAJOR_VERSION) + if !RECluster && !RCEDocker { redisMain, err = startRedis(redisPort) Expect(err).NotTo(HaveOccurred()) @@ -121,18 +139,24 @@ var _ = BeforeSuite(func() { err = startCluster(ctx, cluster) Expect(err).NotTo(HaveOccurred()) } else { - redisPort = rediStackPort - redisAddr = rediStackAddr + redisPort = redisStackPort + redisAddr = redisStackAddr + + if !RECluster { + // populate cluster node information + Expect(configureClusterTopology(ctx, cluster)).NotTo(HaveOccurred()) + } } }) var _ = AfterSuite(func() { if !RECluster { Expect(cluster.Close()).NotTo(HaveOccurred()) + } - for _, p := range processes { - Expect(p.Close()).NotTo(HaveOccurred()) - } + // NOOP if there are no processes registered + for _, p := range processes { + Expect(p.Close()).NotTo(HaveOccurred()) } processes = nil }) @@ -156,8 +180,8 @@ func redisOptions() *redis.Options { ContextTimeoutEnabled: true, MaxRetries: -1, - PoolSize: 10, + PoolSize: 10, PoolTimeout: 30 * time.Second, ConnMaxIdleTime: time.Minute, } diff --git a/monitor_test.go b/monitor_test.go index 96c33bf1ec..ebb784853b 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -22,7 +22,7 @@ var _ = Describe("Monitor command", Label("monitor"), func() { if os.Getenv("RUN_MONITOR_TEST") != "true" { Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") } - client = redis.NewClient(&redis.Options{Addr: ":6379"}) + client = redis.NewClient(&redis.Options{Addr: redisPort}) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) }) @@ -33,7 +33,7 @@ var _ = Describe("Monitor command", Label("monitor"), func() { It("should monitor", Label("monitor"), func() { ress := make(chan string) - client1 := redis.NewClient(&redis.Options{Addr: rediStackAddr}) + client1 := redis.NewClient(&redis.Options{Addr: redisPort}) mn := client1.Monitor(ctx, ress) mn.Start() // Wait for the Redis server to be in monitoring mode. @@ -61,7 +61,7 @@ func TestMonitorCommand(t *testing.T) { } ctx := context.TODO() - client := redis.NewClient(&redis.Options{Addr: ":6379"}) + client := redis.NewClient(&redis.Options{Addr: redisPort}) if err := client.FlushDB(ctx).Err(); err != nil { t.Fatalf("FlushDB failed: %v", err) } @@ -72,8 +72,8 @@ func TestMonitorCommand(t *testing.T) { } }() - ress := make(chan string, 10) // Buffer to prevent blocking - client1 := redis.NewClient(&redis.Options{Addr: ":6379"}) // Adjust the Addr field as necessary + ress := make(chan string, 10) // Buffer to prevent blocking + client1 := redis.NewClient(&redis.Options{Addr: redisPort}) // Adjust the Addr field as necessary mn := client1.Monitor(ctx, ress) mn.Start() // Wait for the Redis server to be in monitoring mode. diff --git a/osscluster_test.go b/osscluster_test.go index 9c3eaba353..93ee464f3d 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -25,6 +25,10 @@ type clusterScenario struct { clients map[string]*redis.Client } +func (s *clusterScenario) slots() []int { + return []int{0, 5461, 10923, 16384} +} + func (s *clusterScenario) masters() []*redis.Client { result := make([]*redis.Client, 3) for pos, port := range s.ports[:3] { @@ -83,35 +87,37 @@ func (s *clusterScenario) newClusterClient( } func (s *clusterScenario) Close() error { + ctx := context.TODO() + for _, master := range s.masters() { + err := master.FlushAll(ctx).Err() + if err != nil { + return err + } + + // since 7.2 forget calls should be propagated, calling only master + // nodes should be sufficient. + for _, nID := range s.nodeIDs { + master.ClusterForget(ctx, nID) + } + } + for _, port := range s.ports { if process, ok := processes[port]; ok { - process.Close() + if process != nil { + process.Close() + } + delete(processes, port) } } + return nil } -func startCluster(ctx context.Context, scenario *clusterScenario) error { - // Start processes and collect node ids - for pos, port := range scenario.ports { - process, err := startRedis(port, "--cluster-enabled", "yes") - if err != nil { - return err - } - - client := redis.NewClient(&redis.Options{ - Addr: ":" + port, - }) - - info, err := client.ClusterNodes(ctx).Result() - if err != nil { - return err - } - - scenario.processes[port] = process - scenario.clients[port] = client - scenario.nodeIDs[pos] = info[:40] +func configureClusterTopology(ctx context.Context, scenario *clusterScenario) error { + err := collectNodeInformation(ctx, scenario) + if err != nil { + return err } // Meet cluster nodes. @@ -122,8 +128,7 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { } } - // Bootstrap masters. - slots := []int{0, 5000, 10000, 16384} + slots := scenario.slots() for pos, master := range scenario.masters() { err := master.ClusterAddSlotsRange(ctx, slots[pos], slots[pos+1]-1).Err() if err != nil { @@ -157,35 +162,36 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { // Wait until all nodes have consistent info. wanted := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8220", + Addr: "127.0.0.1:16600", }, { ID: "", - Addr: "127.0.0.1:8223", + Addr: "127.0.0.1:16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8221", + Addr: "127.0.0.1:16601", }, { ID: "", - Addr: "127.0.0.1:8224", + Addr: "127.0.0.1:16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8222", + Addr: "127.0.0.1:16602", }, { ID: "", - Addr: "127.0.0.1:8225", + Addr: "127.0.0.1:16605", }}, }} + for _, client := range scenario.clients { err := eventually(func() error { res, err := client.ClusterSlots(ctx).Result() @@ -193,7 +199,7 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { return err } return assertSlotsEqual(res, wanted) - }, 30*time.Second) + }, 60*time.Second) if err != nil { return err } @@ -202,6 +208,37 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { return nil } +func collectNodeInformation(ctx context.Context, scenario *clusterScenario) error { + for pos, port := range scenario.ports { + client := redis.NewClient(&redis.Options{ + Addr: ":" + port, + }) + + info, err := client.ClusterNodes(ctx).Result() + if err != nil { + return err + } + + scenario.clients[port] = client + scenario.nodeIDs[pos] = info[:40] + } + return nil +} + +// startCluster start a cluster +func startCluster(ctx context.Context, scenario *clusterScenario) error { + // Start processes and collect node ids + for _, port := range scenario.ports { + process, err := startRedis(port, "--cluster-enabled", "yes") + if err != nil { + return err + } + scenario.processes[port] = process + } + + return configureClusterTopology(ctx, scenario) +} + func assertSlotsEqual(slots, wanted []redis.ClusterSlot) error { outerLoop: for _, s2 := range wanted { @@ -301,17 +338,19 @@ var _ = Describe("ClusterClient", func() { Expect(err).NotTo(HaveOccurred()) } - client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { + err := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { defer GinkgoRecover() Eventually(func() string { return master.Info(ctx, "keyspace").Val() }, 30*time.Second).Should(Or( - ContainSubstring("keys=31"), - ContainSubstring("keys=29"), - ContainSubstring("keys=40"), + ContainSubstring("keys=32"), + ContainSubstring("keys=36"), + ContainSubstring("keys=32"), )) return nil }) + + Expect(err).NotTo(HaveOccurred()) }) It("distributes keys when using EVAL", func() { @@ -327,17 +366,19 @@ var _ = Describe("ClusterClient", func() { Expect(err).NotTo(HaveOccurred()) } - client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { + err := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { defer GinkgoRecover() Eventually(func() string { return master.Info(ctx, "keyspace").Val() }, 30*time.Second).Should(Or( - ContainSubstring("keys=31"), - ContainSubstring("keys=29"), - ContainSubstring("keys=40"), + ContainSubstring("keys=32"), + ContainSubstring("keys=36"), + ContainSubstring("keys=32"), )) return nil }) + + Expect(err).NotTo(HaveOccurred()) }) It("distributes scripts when using Script Load", func() { @@ -347,13 +388,14 @@ var _ = Describe("ClusterClient", func() { script.Load(ctx, client) - client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error { + err := client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error { defer GinkgoRecover() val, _ := script.Exists(ctx, shard).Result() Expect(val[0]).To(Equal(true)) return nil }) + Expect(err).NotTo(HaveOccurred()) }) It("checks all shards when using Script Exists", func() { @@ -727,33 +769,33 @@ var _ = Describe("ClusterClient", func() { wanted := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8220", + Addr: "127.0.0.1:16600", }, { ID: "", - Addr: "127.0.0.1:8223", + Addr: "127.0.0.1:16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8221", + Addr: "127.0.0.1:16601", }, { ID: "", - Addr: "127.0.0.1:8224", + Addr: "127.0.0.1:16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8222", + Addr: "127.0.0.1:16602", }, { ID: "", - Addr: "127.0.0.1:8225", + Addr: "127.0.0.1:16605", }}, }} Expect(assertSlotsEqual(res, wanted)).NotTo(HaveOccurred()) @@ -1122,14 +1164,14 @@ var _ = Describe("ClusterClient", func() { client, err := client.SlaveForKey(ctx, "test") Expect(err).ToNot(HaveOccurred()) info := client.Info(ctx, "server") - Expect(info.Val()).Should(ContainSubstring("tcp_port:8224")) + Expect(info.Val()).Should(ContainSubstring("tcp_port:16604")) }) It("should return correct master for key", func() { client, err := client.MasterForKey(ctx, "test") Expect(err).ToNot(HaveOccurred()) info := client.Info(ctx, "server") - Expect(info.Val()).Should(ContainSubstring("tcp_port:8221")) + Expect(info.Val()).Should(ContainSubstring("tcp_port:16601")) }) assertClusterClient() @@ -1176,18 +1218,18 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard1Port, }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard2Port, }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard3Port, @@ -1230,18 +1272,18 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard1Port, }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard2Port, }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard3Port, @@ -1284,27 +1326,27 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ - Addr: ":8220", + Addr: ":16600", }, { - Addr: ":8223", + Addr: ":16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ - Addr: ":8221", + Addr: ":16601", }, { - Addr: ":8224", + Addr: ":16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ - Addr: ":8222", + Addr: ":16602", }, { - Addr: ":8225", + Addr: ":16605", }}, }} return slots, nil diff --git a/search_commands.go b/search_commands.go index ede084e4e1..9e5928017a 100644 --- a/search_commands.go +++ b/search_commands.go @@ -282,23 +282,30 @@ type FTSearchSortBy struct { Desc bool } +// FTSearchOptions hold options that can be passed to the FT.SEARCH command. +// More information about the options can be found +// in the documentation for FT.SEARCH https://redis.io/docs/latest/commands/ft.search/ type FTSearchOptions struct { - NoContent bool - Verbatim bool - NoStopWords bool - WithScores bool - WithPayloads bool - WithSortKeys bool - Filters []FTSearchFilter - GeoFilter []FTSearchGeoFilter - InKeys []interface{} - InFields []interface{} - Return []FTSearchReturn - Slop int - Timeout int - InOrder bool - Language string - Expander string + NoContent bool + Verbatim bool + NoStopWords bool + WithScores bool + WithPayloads bool + WithSortKeys bool + Filters []FTSearchFilter + GeoFilter []FTSearchGeoFilter + InKeys []interface{} + InFields []interface{} + Return []FTSearchReturn + Slop int + Timeout int + InOrder bool + Language string + Expander string + // Scorer is used to set scoring function, if not set passed, a default will be used. + // The default scorer depends on the Redis version: + // - `BM25` for Redis >= 8 + // - `TFIDF` for Redis < 8 Scorer string ExplainScore bool Payload string diff --git a/search_test.go b/search_test.go index e267c8ae86..a48f45bf0f 100644 --- a/search_test.go +++ b/search_test.go @@ -371,7 +371,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(names).To(ContainElement("John")) }) + // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 + // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + if REDIS_MAJOR_VERSION < 8 { + Skip("(redis major version < 8) default scorer is not BM25") + } + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") + client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.") + + res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.236)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.22471909420069797)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0))) + }) + + // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 + // this test expect redis major version <=7 + It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + if REDIS_MAJOR_VERSION > 7 { + Skip("(redis major version > 7) default scorer is not TFIDF") + } text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) From 12b012255eebf77b47249a4597aa5750a68a3faa Mon Sep 17 00:00:00 2001 From: Shawn Wang <62313353+shawnwgit@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:15:00 -0800 Subject: [PATCH 082/230] Fix race condition in clusterNodes.Addrs() (#3219) Resolve a race condition in the clusterNodes.Addrs() method. Previously, the method returned a reference to a string slice, creating the potential for concurrent reads by the caller while the slice was being modified by the garbage collection process. Co-authored-by: Nedyalko Dyakov --- osscluster.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osscluster.go b/osscluster.go index 72e922a80d..188f50359e 100644 --- a/osscluster.go +++ b/osscluster.go @@ -487,9 +487,11 @@ func (c *clusterNodes) Addrs() ([]string, error) { closed := c.closed //nolint:ifshort if !closed { if len(c.activeAddrs) > 0 { - addrs = c.activeAddrs + addrs = make([]string, len(c.activeAddrs)) + copy(addrs, c.activeAddrs) } else { - addrs = c.addrs + addrs = make([]string, len(c.addrs)) + copy(addrs, c.addrs) } } c.mu.RUnlock() From c0235ccb9d3169f56579827ecead035c72b9d27a Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 3 Feb 2025 19:10:54 +0200 Subject: [PATCH 083/230] feat(tests): validate that ConfigSet and ConfigGet work with Modules (#3258) * Add tests for unified config in Redis 8 * WIP: fix reading FT.CONFIG with RESP3 * add more tests * use search-timeout * move deprecated warnings on the bottom --- .gitignore | 1 + command.go | 48 +++++++++++----- commands_test.go | 138 +++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 30 +++++++--- search_commands.go | 24 ++++++-- search_test.go | 67 ++++++++++++++++++++-- 6 files changed, 274 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 63b21b0b48..f1883206a7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ testdata/* .DS_Store *.tar.gz *.dic +redis8tests.sh diff --git a/command.go b/command.go index f5aad91494..2623a23960 100644 --- a/command.go +++ b/command.go @@ -3862,30 +3862,48 @@ func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { return cmd.val } +// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3 func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadArrayLen() + data, err := rd.ReadReply() if err != nil { return err } + resultMap := map[string]interface{}{} - data := make(map[string]interface{}, n/2) - for i := 0; i < n; i += 2 { - _, err := rd.ReadArrayLen() - if err != nil { - cmd.err = err - } - key, err := rd.ReadString() - if err != nil { - cmd.err = err + switch midResponse := data.(type) { + case map[interface{}]interface{}: // resp3 will return map + for k, v := range midResponse { + stringKey, ok := k.(string) + if !ok { + return fmt.Errorf("redis: invalid map key %#v", k) + } + resultMap[stringKey] = v } - value, err := rd.ReadString() - if err != nil { - cmd.err = err + case []interface{}: // resp2 will return array of arrays + n := len(midResponse) + for i := 0; i < n; i++ { + finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map + if !ok { + return fmt.Errorf("redis: unexpected response %#v", data) + } + m := len(finalArr) + if m%2 != 0 { // since this should be map, keys should be even number + return fmt.Errorf("redis: unexpected response %#v", data) + } + + for j := 0; j < m; j += 2 { + stringKey, ok := finalArr[j].(string) // the first one + if !ok { + return fmt.Errorf("redis: invalid map key %#v", finalArr[i]) + } + resultMap[stringKey] = finalArr[j+1] // second one is value + } } - data[key] = value + default: + return fmt.Errorf("redis: unexpected response %#v", data) } - cmd.val = data + cmd.val = resultMap return nil } diff --git a/commands_test.go b/commands_test.go index 901e96e35e..dacc7f3d52 100644 --- a/commands_test.go +++ b/commands_test.go @@ -344,6 +344,23 @@ var _ = Describe("Commands", func() { Expect(val).NotTo(BeEmpty()) }) + It("should ConfigGet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-*": "search-timeout", + "ts-*": "ts-retention-policy", + "bf-*": "bf-error-rate", + "cf-*": "cf-initial-size", + } + + for prefix, lookup := range expected { + val, err := client.ConfigGet(ctx, prefix).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[lookup]).NotTo(BeEmpty()) + } + }) + It("should ConfigResetStat", Label("NonRedisEnterprise"), func() { r := client.ConfigResetStat(ctx) Expect(r.Err()).NotTo(HaveOccurred()) @@ -362,6 +379,127 @@ var _ = Describe("Commands", func() { Expect(configSet.Val()).To(Equal("OK")) }) + It("should ConfigGet with Modules", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisMajor(8, "config get won't return modules configs before redis 8") + configGet := client.ConfigGet(ctx, "*") + Expect(configGet.Err()).NotTo(HaveOccurred()) + Expect(configGet.Val()).To(HaveKey("maxmemory")) + Expect(configGet.Val()).To(HaveKey("search-timeout")) + Expect(configGet.Val()).To(HaveKey("ts-retention-policy")) + Expect(configGet.Val()).To(HaveKey("bf-error-rate")) + Expect(configGet.Val()).To(HaveKey("cf-initial-size")) + }) + + It("should ConfigSet FT DIALECT", func() { + SkipBeforeRedisMajor(8, "config doesn't include modules before Redis 8") + defaultState, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + + // set to 3 + res, err := client.ConfigSet(ctx, "search-default-dialect", "3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "3"})) + + resGet, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "3"})) + + // set to 2 + res, err = client.ConfigSet(ctx, "search-default-dialect", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + + // set to 1 + res, err = client.ConfigSet(ctx, "search-default-dialect", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + resGet, err = client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "1"})) + + // set to default + res, err = client.ConfigSet(ctx, "search-default-dialect", defaultState["search-default-dialect"]).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + }) + + It("should ConfigSet fail for ReadOnly", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + _, err := client.ConfigSet(ctx, "search-max-doctablesize", "100000").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + defaults := map[string]string{} + expected := map[string]string{ + "search-timeout": "100", + "ts-retention-policy": "2", + "bf-error-rate": "0.13", + "cf-initial-size": "64", + } + + // read the defaults to set them back later + for setting, _ := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + defaults[setting] = val[setting] + } + + // check if new values can be set + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + + for setting, value := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[setting]).To(Equal(value)) + } + + // set back to the defaults + for setting, value := range defaults { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + }) + + It("should Fail ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-timeout": "-100", + "ts-retention-policy": "-10", + "bf-error-rate": "1.5", + "cf-initial-size": "-10", + } + + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(setting))) + Expect(val).To(BeEmpty()) + } + }) + It("should ConfigRewrite", Label("NonRedisEnterprise"), func() { configRewrite := client.ConfigRewrite(ctx) Expect(configRewrite.Err()).NotTo(HaveOccurred()) diff --git a/main_test.go b/main_test.go index 6b3b563a02..a326960a0f 100644 --- a/main_test.go +++ b/main_test.go @@ -73,7 +73,19 @@ var RCEDocker = false // Notes the major version of redis we are executing tests. // This can be used before we change the bsm fork of ginkgo for one, // which have support for label sets, so we can filter tests per redis major version. -var REDIS_MAJOR_VERSION = 7 +var RedisMajorVersion = 7 + +func SkipBeforeRedisMajor(version int, msg string) { + if RedisMajorVersion < version { + Skip(fmt.Sprintf("(redis major version < %d) %s", version, msg)) + } +} + +func SkipAfterRedisMajor(version int, msg string) { + if RedisMajorVersion > version { + Skip(fmt.Sprintf("(redis major version > %d) %s", version, msg)) + } +} func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -92,16 +104,20 @@ var _ = BeforeSuite(func() { RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) - REDIS_MAJOR_VERSION, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) - if REDIS_MAJOR_VERSION == 0 { - REDIS_MAJOR_VERSION = 7 + RedisMajorVersion, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + + if RedisMajorVersion == 0 { + RedisMajorVersion = 7 } - Expect(REDIS_MAJOR_VERSION).To(BeNumerically(">=", 6)) - Expect(REDIS_MAJOR_VERSION).To(BeNumerically("<=", 8)) fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_MAJOR_VERSION: %v\n", REDIS_MAJOR_VERSION) + fmt.Printf("REDIS_MAJOR_VERSION: %v\n", RedisMajorVersion) + + if RedisMajorVersion < 6 || RedisMajorVersion > 8 { + panic("incorrect or not supported redis major version") + } + if !RECluster && !RCEDocker { redisMain, err = startRedis(redisPort) diff --git a/search_commands.go b/search_commands.go index 9e5928017a..1312a78f09 100644 --- a/search_commands.go +++ b/search_commands.go @@ -831,20 +831,32 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool return cmd } -// FTConfigGet - Retrieves the value of a RediSearch configuration parameter. +// Retrieves the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to retrieve. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/) +// For more information, please refer to the Redis [FT.CONFIG GET] documentation. +// +// Deprecated: FTConfigGet is deprecated in Redis 8. +// All configuration will be done with the CONFIG GET command. +// For more information check [Client.ConfigGet] and [CONFIG GET Documentation] +// +// [CONFIG GET Documentation]: https://redis.io/commands/config-get/ +// [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/ func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd { cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) _ = c(ctx, cmd) return cmd } -// FTConfigSet - Sets the value of a RediSearch configuration parameter. +// Sets the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG SET]: (https://redis.io/commands/ft.config-set/) +// For more information, please refer to the Redis [FT.CONFIG SET] documentation. +// +// Deprecated: FTConfigSet is deprecated in Redis 8. +// All configuration will be done with the CONFIG SET command. +// For more information check [Client.ConfigSet] and [CONFIG SET Documentation] +// +// [CONFIG SET Documentation]: https://redis.io/commands/config-set/ +// [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/ func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) _ = c(ctx, cmd) diff --git a/search_test.go b/search_test.go index a48f45bf0f..0a06ffef8e 100644 --- a/search_test.go +++ b/search_test.go @@ -374,9 +374,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION < 8 { - Skip("(redis major version < 8) default scorer is not BM25") - } + SkipBeforeRedisMajor(8, "default scorer is not BM25") + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -418,9 +417,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version <=7 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION > 7 { - Skip("(redis major version > 7) default scorer is not TFIDF") - } + SkipAfterRedisMajor(7, "default scorer is not TFIDF") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -1015,6 +1012,24 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) + It("should FTConfigGet return multiple fields", Label("search", "NonRedisEnterprise"), func() { + res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + }) + It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() Expect(err).NotTo(HaveOccurred()) @@ -1471,6 +1486,46 @@ func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []str // Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) // }) +var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "NonRedisEnterprise"), func() { + + var clientResp2 *redis.Client + var clientResp3 *redis.Client + BeforeEach(func() { + clientResp2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + clientResp3 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true}) + Expect(clientResp3.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(clientResp2.Close()).NotTo(HaveOccurred()) + Expect(clientResp3.Close()).NotTo(HaveOccurred()) + }) + + It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + val, err := clientResp3.FTConfigSet(ctx, "TIMEOUT", "100").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res2, err := clientResp2.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + + res3, err := clientResp3.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + }) + + It("should FTConfigGet all resp2 and resp3", Label("search", "NonRedisEnterprise"), func() { + res2, err := clientResp2.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + + res3, err := clientResp3.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res3)).To(BeEquivalentTo(len(res2))) + Expect(res2["DEFAULT_DIALECT"]).To(BeEquivalentTo(res2["DEFAULT_DIALECT"])) + }) +}) + var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { ctx := context.TODO() var client *redis.Client From b83216f26a4c65ac9a2a0e95d16a73003ca3ddf0 Mon Sep 17 00:00:00 2001 From: ZhuHaiCheng Date: Tue, 4 Feb 2025 01:29:02 +0800 Subject: [PATCH 084/230] chore: fix some comments (#3226) Signed-off-by: zhuhaicity Co-authored-by: Nedyalko Dyakov --- hash_commands.go | 2 +- search_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hash_commands.go b/hash_commands.go index dcffdcdd98..6596c6f5f7 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -225,7 +225,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati return cmd } -// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds. // It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. // For more information - https://redis.io/commands/hexpire/ diff --git a/search_test.go b/search_test.go index 0a06ffef8e..a409fc78a2 100644 --- a/search_test.go +++ b/search_test.go @@ -136,7 +136,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "txt") - client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis") + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result() Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(BeEquivalentTo(int64(1))) @@ -482,7 +482,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { WaitForIndexing(client, "idx1") client.HSet(ctx, "search", "title", "RediSearch", - "body", "Redisearch impements a search engine on top of redis", + "body", "Redisearch implements a search engine on top of redis", "parent", "redis", "random_num", 10) client.HSet(ctx, "ai", "title", "RedisAI", From 07548a6d51e18f690bacde5b4ba2d48ce8b1a8d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:28:08 +0200 Subject: [PATCH 085/230] chore(deps): bump github.com/cespare/xxhash/v2 from 2.2.0 to 2.3.0 (#2964) Bumps [github.com/cespare/xxhash/v2](https://github.com/cespare/xxhash) from 2.2.0 to 2.3.0. - [Commits](https://github.com/cespare/xxhash/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: github.com/cespare/xxhash/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c1d9037acc..1492d27098 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/cespare/xxhash/v2 v2.2.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ) diff --git a/go.sum b/go.sum index 21b4f64ee2..4db68f6d4f 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= From 40ed9dc54f24b651156af557c1fae14d8aee9436 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Tue, 4 Feb 2025 10:34:08 +0100 Subject: [PATCH 086/230] feat(options): add skip_verify param (#3216) * feat(options): Add skip_verify param When parsing a URL, add a "skip_verify" query param to disable TLS certificate verification. Inspired by various Go drivers: * ClickHouse: https://github.com/ClickHouse/clickhouse-go/blob/v2.30.0/clickhouse_options.go#L259 * MongoDB: https://github.com/mongodb/mongo-go-driver/blob/v2.0.0/x/mongo/driver/connstring/connstring.go#L609 * MySQL: https://github.com/go-sql-driver/mysql/blob/v1.8.1/dsn.go#L175 Signed-off-by: Julien Riou * docs(options): Add skip_verify to ParseURL Signed-off-by: Julien Riou --------- Signed-off-by: Julien Riou Co-authored-by: Nedyalko Dyakov --- options.go | 4 ++++ options_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/options.go b/options.go index 8ba74ccd1a..b9701702f5 100644 --- a/options.go +++ b/options.go @@ -267,6 +267,7 @@ func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, er // URL attributes (scheme, host, userinfo, resp.), query parameters using these // names will be treated as unknown parameters // - unknown parameter names will result in an error +// - use "skip_verify=true" to ignore TLS certificate validation // // Examples: // @@ -487,6 +488,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) { if q.err != nil { return nil, q.err } + if o.TLSConfig != nil && q.has("skip_verify") { + o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify") + } // any parameters left? if r := q.remaining(); len(r) > 0 { diff --git a/options_test.go b/options_test.go index 1db36fdb4a..d46ecc8583 100644 --- a/options_test.go +++ b/options_test.go @@ -30,6 +30,9 @@ func TestParseURL(t *testing.T) { }, { url: "rediss://localhost:123", o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{ /* no deep comparison */ }}, + }, { + url: "rediss://localhost:123/?skip_verify=true", + o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{InsecureSkipVerify: true}}, }, { url: "redis://:bar@localhost:123", o: &Options{Addr: "localhost:123", Password: "bar"}, From d4ddfe9c893844bbcab20e4f1b89d340c11a5564 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Wed, 5 Feb 2025 15:44:09 +0200 Subject: [PATCH 087/230] feat(command): add ACL commands, validate module categories exist (#3262) * add ACL{SetUser,DelUser,List} commands * test presence of categories in acl cat * code cleanup * add basic acl tests * add acl modules tests * reset acl log before test * refactor acl tests * fix clientkillbyage test --- acl_commands.go | 54 ++++++ acl_commands_test.go | 449 +++++++++++++++++++++++++++++++++++++++++++ commands_test.go | 55 +----- 3 files changed, 505 insertions(+), 53 deletions(-) create mode 100644 acl_commands_test.go diff --git a/acl_commands.go b/acl_commands.go index 06847be2ed..9cb800bb3b 100644 --- a/acl_commands.go +++ b/acl_commands.go @@ -4,8 +4,20 @@ import "context" type ACLCmdable interface { ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd + ACLLog(ctx context.Context, count int64) *ACLLogCmd ACLLogReset(ctx context.Context) *StatusCmd + + ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd + ACLDelUser(ctx context.Context, username string) *IntCmd + ACLList(ctx context.Context) *StringSliceCmd + + ACLCat(ctx context.Context) *StringSliceCmd + ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd +} + +type ACLCatArgs struct { + Category string } func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { @@ -33,3 +45,45 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { _ = c(ctx, cmd) return cmd } + +func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd { + cmd := NewIntCmd(ctx, "acl", "deluser", username) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd { + args := make([]interface{}, 3+len(rules)) + args[0] = "acl" + args[1] = "setuser" + args[2] = username + for i, rule := range rules { + args[i+3] = rule + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "list") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "cat") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd { + // if there is a category passed, build new cmd, if there isn't - use the ACLCat method + if options != nil && options.Category != "" { + cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category) + _ = c(ctx, cmd) + return cmd + } + + return c.ACLCat(ctx) +} diff --git a/acl_commands_test.go b/acl_commands_test.go new file mode 100644 index 0000000000..8464558318 --- /dev/null +++ b/acl_commands_test.go @@ -0,0 +1,449 @@ +package redis_test + +import ( + "context" + + "github.com/redis/go-redis/v9" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" +) + +var TestUserName string = "goredis" +var _ = Describe("ACL", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + It("should ACL LOG", Label("NonRedisEnterprise"), func() { + Expect(client.ACLLogReset(ctx).Err()).NotTo(HaveOccurred()) + err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() + Expect(err).NotTo(HaveOccurred()) + + clientAcl := redis.NewClient(redisOptions()) + clientAcl.Options().Username = "test" + clientAcl.Options().Password = "test" + clientAcl.Options().DB = 0 + _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() + _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() + _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() + + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(4)) + + for _, entry := range logEntries { + Expect(entry.Reason).To(Equal("command")) + Expect(entry.Context).To(Equal("toplevel")) + Expect(entry.Object).NotTo(BeEmpty()) + Expect(entry.Username).To(Equal("test")) + Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) + Expect(entry.ClientInfo).NotTo(BeNil()) + Expect(entry.EntryID).To(BeNumerically(">=", 0)) + Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) + Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) + } + + limitedLogEntries, err := client.ACLLog(ctx, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(limitedLogEntries)).To(Equal(2)) + + // cleanup after creating the user + err = client.Do(ctx, "acl", "deluser", "test").Err() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { + // Call ACL LOG RESET + resetCmd := client.ACLLogReset(ctx) + Expect(resetCmd.Err()).NotTo(HaveOccurred()) + Expect(resetCmd.Val()).To(Equal("OK")) + + // Verify that the log is empty after the reset + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(0)) + }) + +}) +var _ = Describe("ACL user commands", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("list only default user", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + }) + + It("setuser and deluser", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + + add, err := client.ACLSetUser(ctx, TestUserName, "nopass", "on", "allkeys", "+set", "+get").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + resAfter, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfter).To(HaveLen(2)) + Expect(resAfter[1]).To(ContainSubstring(TestUserName)) + + deletedN, err := client.ACLDelUser(ctx, TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(deletedN).To(BeNumerically("==", 1)) + + resAfterDeletion, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfterDeletion).To(HaveLen(1)) + Expect(resAfterDeletion[0]).To(BeEquivalentTo(res[0])) + }) + + It("should acl dryrun", func() { + dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") + Expect(dryRun.Err()).NotTo(HaveOccurred()) + Expect(dryRun.Val()).To(Equal("OK")) + }) +}) + +var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + opt.UnstableResp3 = true + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("reset permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + _, err = connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + }) + + It("add write permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+SET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can write + v, err := connection.Set(ctx, "anykey", "anyvalue", 0).Result() + Expect(err).ToNot(HaveOccurred()) + Expect(v).To(Equal("OK")) + + // but can't read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(value).To(BeEmpty()) + }) + + It("add read permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+GET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(Equal("anyvalue")) + + // but can't delete + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(del).ToNot(Equal(1)) + }) + + It("add del permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+DEL", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(del).To(BeEquivalentTo(1)) + }) + + It("set permissions for module commands", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+FT.SEARCH", + "-FT.DROPINDEX", + "+json.set", + "+json.get", + "-json.clear", + "+bf.reserve", + "-bf.info", + "+cf.reserve", + "+cms.initbydim", + "+topk.reserve", + "+tdigest.create", + "+ts.create", + "-ts.info", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + + // no perm for dropindex + err = c.FTDropIndex(ctx, "txt").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + + // no perm for json clear + err = c.JSONClear(ctx, "foo", "$").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + + // no perm for info + err = c.BFInfo(ctx, "bloom").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // noperm for ts.info + err = c.TSInfo(ctx, "tsts").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + It("set permissions for module categories", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+@search", + "+@json", + "+@bloom", + "+@cuckoo", + "+@topk", + "+@cms", + "+@timeseries", + "+@tdigest", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + // perm for dropindex + Expect(c.FTDropIndex(ctx, "txt").Err()).NotTo(HaveOccurred()) + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + // perm for json clear + Expect(c.JSONClear(ctx, "foo", "$").Err()).NotTo(HaveOccurred()) + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + // perm for info + Expect(c.BFInfo(ctx, "bloom").Err()).NotTo(HaveOccurred()) + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // perm for ts.info + Expect(c.TSInfo(ctx, "tsts").Err()).NotTo(HaveOccurred()) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("ACL Categories", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("lists acl categories and subcategories", func() { + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res)).To(BeNumerically(">", 20)) + Expect(res).To(ContainElements( + "read", + "write", + "keyspace", + "dangerous", + "slow", + "set", + "sortedset", + "list", + "hash", + )) + + res, err = client.ACLCatArgs(ctx, &redis.ACLCatArgs{Category: "read"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement("get")) + }) + + It("lists acl categories and subcategories with Modules", func() { + SkipBeforeRedisMajor(8, "modules are included in acl for redis version >= 8") + aclTestCase := map[string]string{ + "search": "FT.CREATE", + "bloom": "bf.add", + "json": "json.get", + "cuckoo": "cf.insert", + "cms": "cms.query", + "topk": "topk.list", + "tdigest": "tdigest.rank", + "timeseries": "ts.range", + } + var cats []interface{} + + for cat, subitem := range aclTestCase { + cats = append(cats, cat) + + res, err := client.ACLCatArgs(ctx, &redis.ACLCatArgs{ + Category: cat, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement(subitem)) + } + + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElements(cats...)) + }) +}) diff --git a/commands_test.go b/commands_test.go index dacc7f3d52..404ffd02be 100644 --- a/commands_test.go +++ b/commands_test.go @@ -211,13 +211,13 @@ var _ = Describe("Commands", func() { select { case <-done: Fail("BLPOP is not blocked.") - case <-time.After(2 * time.Second): + case <-time.After(1 * time.Second): // ok } killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)))) + Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)), Equal(int64(4)))) select { case <-done: @@ -2228,12 +2228,6 @@ var _ = Describe("Commands", func() { Expect(replace.Val()).To(Equal(int64(1))) }) - It("should acl dryrun", func() { - dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") - Expect(dryRun.Err()).NotTo(HaveOccurred()) - Expect(dryRun.Val()).To(Equal("OK")) - }) - It("should fail module loadex", Label("NonRedisEnterprise"), func() { dryRun := client.ModuleLoadex(ctx, &redis.ModuleLoadexConfig{ Path: "/path/to/non-existent-library.so", @@ -2281,51 +2275,6 @@ var _ = Describe("Commands", func() { Expect(args).To(Equal(expectedArgs)) }) - - It("should ACL LOG", Label("NonRedisEnterprise"), func() { - err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() - Expect(err).NotTo(HaveOccurred()) - - clientAcl := redis.NewClient(redisOptions()) - clientAcl.Options().Username = "test" - clientAcl.Options().Password = "test" - clientAcl.Options().DB = 0 - _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() - _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() - _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() - - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(4)) - - for _, entry := range logEntries { - Expect(entry.Reason).To(Equal("command")) - Expect(entry.Context).To(Equal("toplevel")) - Expect(entry.Object).NotTo(BeEmpty()) - Expect(entry.Username).To(Equal("test")) - Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) - Expect(entry.ClientInfo).NotTo(BeNil()) - Expect(entry.EntryID).To(BeNumerically(">=", 0)) - Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) - Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) - } - - limitedLogEntries, err := client.ACLLog(ctx, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(limitedLogEntries)).To(Equal(2)) - }) - - It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { - // Call ACL LOG RESET - resetCmd := client.ACLLogReset(ctx) - Expect(resetCmd.Err()).NotTo(HaveOccurred()) - Expect(resetCmd.Val()).To(Equal("OK")) - - // Verify that the log is empty after the reset - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(0)) - }) }) Describe("hashes", func() { From ed8b629586e688841d23a18d1cbd0e77c0032fbf Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:31:01 +0000 Subject: [PATCH 088/230] DOC-4734 added geo indexing examples (#3240) * DOC-4734 added geo indexing examples * DOC-4734 delete keys before starting tests --------- Co-authored-by: Nedyalko Dyakov --- doctests/geo_index_test.go | 206 +++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 doctests/geo_index_test.go diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go new file mode 100644 index 0000000000..9c38ba9d3e --- /dev/null +++ b/doctests/geo_index_test.go @@ -0,0 +1,206 @@ +// EXAMPLE: geoindex +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoindex() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // REMOVE_START + rdb.FTDropIndex(ctx, "productidx") + rdb.FTDropIndex(ctx, "geomidx") + rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") + // REMOVE_END + + // STEP_START create_geo_idx + geoCreateResult, err := rdb.FTCreate(ctx, + "productidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"product:"}, + }, + &redis.FieldSchema{ + FieldName: "$.location", + As: "location", + FieldType: redis.SearchFieldTypeGeo, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoCreateResult) // >>> OK + // STEP_END + + // STEP_START add_geo_json + prd46885 := map[string]interface{}{ + "description": "Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "-104.991531, 39.742043", + } + + gjResult1, err := rdb.JSONSet(ctx, "product:46885", "$", prd46885).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult1) // >>> OK + + prd46886 := map[string]interface{}{ + "description": "Bright Green Socks", + "price": 25.50, + "city": "Fort Collins", + "location": "-105.0618814,40.5150098", + } + + gjResult2, err := rdb.JSONSet(ctx, "product:46886", "$", prd46886).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult2) // >>> OK + // STEP_END + + // STEP_START geo_query + geoQueryResult, err := rdb.FTSearch(ctx, "productidx", + "@location:[-104.800644 38.846127 100 mi]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoQueryResult) + // >>> {1 [{product:46885... + // STEP_END + + // STEP_START create_gshape_idx + geomCreateResult, err := rdb.FTCreate(ctx, "geomidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"shape:"}, + }, + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.geom", + As: "geom", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomCreateResult) // >>> OK + // STEP_END + + // STEP_START add_gshape_json + shape1 := map[string]interface{}{ + "name": "Green Square", + "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + } + + gmjResult1, err := rdb.JSONSet(ctx, "shape:1", "$", shape1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult1) // >>> OK + + shape2 := map[string]interface{}{ + "name": "Red Rectangle", + "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))", + } + + gmjResult2, err := rdb.JSONSet(ctx, "shape:2", "$", shape2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult2) // >>> OK + + shape3 := map[string]interface{}{ + "name": "Blue Triangle", + "geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))", + } + + gmjResult3, err := rdb.JSONSet(ctx, "shape:3", "$", shape3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult3) // >>> OK + + shape4 := map[string]interface{}{ + "name": "Purple Point", + "geom": "POINT (2 2)", + } + + gmjResult4, err := rdb.JSONSet(ctx, "shape:4", "$", shape4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult4) // >>> OK + // STEP_END + + // STEP_START gshape_query + geomQueryResult, err := rdb.FTSearchWithArgs(ctx, "geomidx", + "(-@name:(Green Square) @geom:[WITHIN $qshape])", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + }, + DialectVersion: 4, + Limit: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomQueryResult) + // >>> {1 [{shape:4... + // STEP_END + + // Output: + // OK + // OK + // OK + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // OK + // OK + // OK + // OK + // OK + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} +} From 52672e50af94c7e4a98f29162474e384142a8d69 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:33:11 +0000 Subject: [PATCH 089/230] DOC-4799 fixed capped list example (#3260) Co-authored-by: Nedyalko Dyakov --- doctests/list_tutorial_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go index 908469ce05..bec1e16435 100644 --- a/doctests/list_tutorial_test.go +++ b/doctests/list_tutorial_test.go @@ -388,7 +388,7 @@ func ExampleClient_ltrim() { // REMOVE_END // STEP_START ltrim - res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + res27, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() if err != nil { panic(err) @@ -410,13 +410,13 @@ func ExampleClient_ltrim() { panic(err) } - fmt.Println(res29) // >>> [bike:5 bike:4 bike:3] + fmt.Println(res29) // >>> [bike:1 bike:2 bike:3] // STEP_END // Output: // 5 // OK - // [bike:5 bike:4 bike:3] + // [bike:1 bike:2 bike:3] } func ExampleClient_ltrim_end_of_list() { From 85bccdcf258c1ce0664bb8034b750e7e1d2d4c71 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:34:51 +0000 Subject: [PATCH 090/230] DOC-4331 added full text query examples (#3256) * DOC-4331 added full text query examples * DOC-4331 made tests deterministic with sort --------- Co-authored-by: Nedyalko Dyakov --- doctests/query_ft_test.go | 331 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 doctests/query_ft_test.go diff --git a/doctests/query_ft_test.go b/doctests/query_ft_test.go new file mode 100644 index 0000000000..095230f739 --- /dev/null +++ b/doctests/query_ft_test.go @@ -0,0 +1,331 @@ +// EXAMPLE: query_ft +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_ft() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START ft1 + res1, err := rdb.FTSearch(ctx, + "idx:bicycle", "@description: kids", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 2 + + sort.Slice(res1.Docs, func(i, j int) bool { + return res1.Docs[i].ID < res1.Docs[j].ID + }) + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:1 + // >>> bicycle:2 + // STEP_END + + // STEP_START ft2 + res2, err := rdb.FTSearch(ctx, + "idx:bicycle", "@model: ka*", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:4 + // STEP_END + + // STEP_START ft3 + res3, err := rdb.FTSearch(ctx, + "idx:bicycle", "@brand: *bikes", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 2 + + sort.Slice(res3.Docs, func(i, j int) bool { + return res3.Docs[i].ID < res3.Docs[j].ID + }) + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:4 + // >>> bicycle:6 + // STEP_END + + // STEP_START ft4 + res4, err := rdb.FTSearch(ctx, + "idx:bicycle", "%optamized%", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4.Total) // >>> 1 + + for _, doc := range res4.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:3 + // STEP_END + + // STEP_START ft5 + res5, err := rdb.FTSearch(ctx, + "idx:bicycle", "%%optamised%%", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5.Total) // >>> 1 + + for _, doc := range res5.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:3 + // STEP_END + + // Output: + // 2 + // bicycle:1 + // bicycle:2 + // 1 + // bicycle:4 + // 2 + // bicycle:4 + // bicycle:6 + // 1 + // bicycle:3 + // 1 + // bicycle:3 +} From 3c78310e837ecc319cbe49d0c2bf568d75f55bff Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:42:38 +0000 Subject: [PATCH 091/230] DOC-4332 added geo query examples (#3257) Co-authored-by: Nedyalko Dyakov --- doctests/query_geo_test.go | 327 +++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 doctests/query_geo_test.go diff --git a/doctests/query_geo_test.go b/doctests/query_geo_test.go new file mode 100644 index 0000000000..7e880aead1 --- /dev/null +++ b/doctests/query_geo_test.go @@ -0,0 +1,327 @@ +// EXAMPLE: query_geo +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_geo() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + &redis.FieldSchema{ + FieldName: "$.store_location", + As: "store_location", + FieldType: redis.SearchFieldTypeGeo, + }, + &redis.FieldSchema{ + FieldName: "$.pickup_zone", + As: "pickup_zone", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START geo1 + res1, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "@store_location:[$lon $lat $radius $units]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "lon": -0.1778, + "lat": 51.5524, + "radius": 20, + "units": "mi", + }, + DialectVersion: 2, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 1 + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // STEP_END + + // STEP_START geo2 + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@pickup_zone:[CONTAINS $bike]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "bike": "POINT(-0.1278 51.5074)", + }, + DialectVersion: 3, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // STEP_END + + // STEP_START geo3 + res3, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@pickup_zone:[WITHIN $europe]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "europe": "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))", + }, + DialectVersion: 3, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + sort.Slice(res3.Docs, func(i, j int) bool { + return res3.Docs[i].ID < res3.Docs[j].ID + }) + + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // >>> bicycle:9 + // STEP_END + + // Output: + // 1 + // bicycle:5 + // 1 + // bicycle:5 + // 5 + // bicycle:5 + // bicycle:6 + // bicycle:7 + // bicycle:8 + // bicycle:9 +} From c42db42f159edd5a70433aecfb403127bb77f6e0 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:52:49 +0000 Subject: [PATCH 092/230] DOC-4300 added exact match examples (#3251) Co-authored-by: Nedyalko Dyakov --- doctests/query_em_test.go | 363 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 doctests/query_em_test.go diff --git a/doctests/query_em_test.go b/doctests/query_em_test.go new file mode 100644 index 0000000000..d4267df4f9 --- /dev/null +++ b/doctests/query_em_test.go @@ -0,0 +1,363 @@ +// EXAMPLE: query_em +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_em() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START em1 + res1, err := rdb.FTSearch(ctx, + "idx:bicycle", "@price:[270 270]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 1 + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:0 + + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "*", + &redis.FTSearchOptions{ + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: 270, + Max: 270, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:0 + // STEP_END + + // STEP_START em2 + res3, err := rdb.FTSearch(ctx, + "idx:bicycle", "@condition:{new}", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // >>> bicycle:0 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // STEP_END + + // STEP_START em3 + res4, err := rdb.FTCreate(ctx, + "idx:email", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"key:"}, + }, + &redis.FieldSchema{ + FieldName: "$.email", + As: "email", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> OK + + res5, err := rdb.JSONSet(ctx, "key:1", "$", + map[string]interface{}{ + "email": "test@redis.com", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> OK + + res6, err := rdb.FTSearch(ctx, "idx:email", + "@email:{test\\@redis\\.com}", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6.Total) // >>> 1 + // STEP_END + + // STEP_START em4 + res7, err := rdb.FTSearch(ctx, + "idx:bicycle", "@description:\"rough terrain\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7.Total) // >>> 1 + + for _, doc := range res7.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:8 + // STEP_END + + // Output: + // 1 + // bicycle:0 + // 1 + // bicycle:0 + // 5 + // bicycle:5 + // bicycle:0 + // bicycle:6 + // bicycle:7 + // bicycle:8 + // OK + // OK + // 1 + // 1 + // bicycle:8 +} From e4173a47839b237cc3541160a78ac292b46ae3a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:02:15 +0200 Subject: [PATCH 093/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3227) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.45.0 to 0.46.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.45.0...0.46.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 977f8c5c11..95cfdfaa04 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.45.0 + uses: rojopolis/spellcheck-github-actions@0.46.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 7816fdda4b4dfb92969557d383f571f6485aca7d Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 11:29:26 +0200 Subject: [PATCH 094/230] fix(aggregate, search): ft.aggregate bugfixes (#3263) * fix: rearange args for ft.aggregate apply should be before any groupby or sortby * improve test * wip: add scorer and addscores * enable all tests * fix ftsearch with count test * make linter happy * Addscores is available in later redisearch releases. For safety state it is available in redis ce 8 * load an apply seem to break scorer and addscores --- search_commands.go | 81 +++++++++++++++++++++++++---------- search_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 25 deletions(-) diff --git a/search_commands.go b/search_commands.go index 1312a78f09..df12bb3f97 100644 --- a/search_commands.go +++ b/search_commands.go @@ -240,13 +240,20 @@ type FTAggregateWithCursor struct { } type FTAggregateOptions struct { - Verbatim bool - LoadAll bool - Load []FTAggregateLoad - Timeout int - GroupBy []FTAggregateGroupBy - SortBy []FTAggregateSortBy - SortByMax int + Verbatim bool + LoadAll bool + Load []FTAggregateLoad + Timeout int + GroupBy []FTAggregateGroupBy + SortBy []FTAggregateSortBy + SortByMax int + // Scorer is used to set scoring function, if not set passed, a default will be used. + // The default scorer depends on the Redis version: + // - `BM25` for Redis >= 8 + // - `TFIDF` for Redis < 8 + Scorer string + // AddScores is available in Redis CE 8 + AddScores bool Apply []FTAggregateApply LimitOffset int Limit int @@ -490,6 +497,15 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } + + if options.Scorer != "" { + queryArgs = append(queryArgs, "SCORER", options.Scorer) + } + + if options.AddScores { + queryArgs = append(queryArgs, "ADDSCORES") + } + if options.LoadAll && options.Load != nil { panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } @@ -505,9 +521,18 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } } } + if options.Timeout > 0 { queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) } + + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } + } + if options.GroupBy != nil { for _, groupBy := range options.GroupBy { queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) @@ -549,12 +574,6 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.SortByMax > 0 { queryArgs = append(queryArgs, "MAX", options.SortByMax) } - for _, apply := range options.Apply { - queryArgs = append(queryArgs, "APPLY", apply.Field) - if apply.As != "" { - queryArgs = append(queryArgs, "AS", apply.As) - } - } if options.LimitOffset > 0 { queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) } @@ -581,6 +600,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, key, value) } } + if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) } @@ -661,11 +681,12 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { cmd.err = err - return nil + return err } cmd.val, err = ProcessAggregateResult(data) if err != nil { cmd.err = err + return err } return nil } @@ -681,6 +702,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Verbatim { args = append(args, "VERBATIM") } + if options.Scorer != "" { + args = append(args, "SCORER", options.Scorer) + } + if options.AddScores { + args = append(args, "ADDSCORES") + } if options.LoadAll && options.Load != nil { panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } @@ -699,6 +726,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) } + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) + } + } if options.GroupBy != nil { for _, groupBy := range options.GroupBy { args = append(args, "GROUPBY", len(groupBy.Fields)) @@ -740,12 +773,6 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.SortByMax > 0 { args = append(args, "MAX", options.SortByMax) } - for _, apply := range options.Apply { - args = append(args, "APPLY", apply.Field) - if apply.As != "" { - args = append(args, "AS", apply.As) - } - } if options.LimitOffset > 0 { args = append(args, "LIMIT", options.LimitOffset) } @@ -1693,7 +1720,8 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { // FTSearch - Executes a search query on an index. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. -// For more information, please refer to the Redis documentation: +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// // [FT.SEARCH]: (https://redis.io/commands/ft.search/) func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} @@ -1704,6 +1732,12 @@ func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSe type SearchQuery []interface{} +// FTSearchQuery - Executes a search query on an index with additional options. +// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, +// and the 'options' parameter specifies additional options for the search. +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// +// [FT.SEARCH]: (https://redis.io/commands/ft.search/) func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { queryArgs := []interface{}{query} if options != nil { @@ -1816,7 +1850,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { // FTSearchWithArgs - Executes a search query on an index with additional options. // The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, // and the 'options' parameter specifies additional options for the search. -// For more information, please refer to the Redis documentation: +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// // [FT.SEARCH]: (https://redis.io/commands/ft.search/) func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} @@ -1908,7 +1943,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin } } if options.SortByWithCount { - args = append(args, "WITHCOUT") + args = append(args, "WITHCOUNT") } } if options.LimitOffset >= 0 && options.Limit > 0 { diff --git a/search_test.go b/search_test.go index a409fc78a2..e4e5521526 100644 --- a/search_test.go +++ b/search_test.go @@ -2,6 +2,8 @@ package redis_test import ( "context" + "fmt" + "strconv" "time" . "github.com/bsm/ginkgo/v2" @@ -127,8 +129,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res3.Total).To(BeEquivalentTo(int64(0))) + Expect(res3.Total).To(BeEquivalentTo(int64(3))) + res4, err := client.FTSearchWithArgs(ctx, "num", "notpresentf00", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeEquivalentTo(int64(0))) }) It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { @@ -640,6 +645,100 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) }) + It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { + SkipBeforeRedisMajor(8, "ADDSCORES is available in Redis CE 8") + title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false} + description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "product:1", "title", "New Gaming Laptop", "description", "this is not a desktop") + client.HSet(ctx, "product:2", "title", "Super Old Not Gaming Laptop", "description", "this laptop is not a new laptop but it is a laptop") + client.HSet(ctx, "product:3", "title", "Office PC", "description", "office desktop pc") + + options := &redis.FTAggregateOptions{ + AddScores: true, + Scorer: "BM25", + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@__score", + Desc: true, + }}, + } + + res, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Rows)).To(BeEquivalentTo(2)) + score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + Expect(score1).To(BeNumerically(">", score2)) + + optionsDM := &redis.FTAggregateOptions{ + AddScores: true, + Scorer: "DISMAX", + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@__score", + Desc: true, + }}, + } + + resDM, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", optionsDM).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDM).ToNot(BeNil()) + Expect(len(resDM.Rows)).To(BeEquivalentTo(2)) + score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + Expect(score1DM).To(BeNumerically(">", score2DM)) + + Expect(score1DM).To(BeEquivalentTo(float64(4))) + Expect(score2DM).To(BeEquivalentTo(float64(1))) + Expect(score1).NotTo(BeEquivalentTo(score1DM)) + Expect(score2).NotTo(BeEquivalentTo(score2DM)) + }) + + It("should FTAggregate apply and groupby", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} + num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // 6 feb + client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "1738823999") + + // 12 feb + client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739342399") + client.HSet(ctx, "doc3", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739353199") + + reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: "perDay"} + + options := &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{{Field: "floor(@CreatedDateTimeUTC /(60*60*24))", As: "TimestampAsDay"}}, + GroupBy: []redis.FTAggregateGroupBy{{ + Fields: []interface{}{"@TimestampAsDay"}, + Reduce: []redis.FTAggregateReducer{reducer}, + }}, + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@perDay", + Desc: true, + }}, + } + + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Rows)).To(BeEquivalentTo(2)) + Expect(res.Rows[0].Fields["perDay"]).To(BeEquivalentTo("2")) + Expect(res.Rows[1].Fields["perDay"]).To(BeEquivalentTo("1")) + }) + It("should FTAggregate apply", Label("search", "ftaggregate"), func() { text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} @@ -684,7 +783,6 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19")) Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25")) } - }) It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() { From 162b52675a74b9b0dcabcc4b01b7ccff4d16fabf Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 12:09:49 +0200 Subject: [PATCH 095/230] fix: add unstableresp3 to cluster client (#3266) * fix: add unstableresp3 to cluster client * propagate unstableresp3 * proper test that will ignore error, but fail if client panics * add separate test for clusterclient constructor --- options.go | 2 +- osscluster.go | 6 +++++- universal.go | 1 + universal_test.go | 22 ++++++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/options.go b/options.go index b9701702f5..a350a02f9b 100644 --- a/options.go +++ b/options.go @@ -154,7 +154,7 @@ type Options struct { // Add suffix to client name. Default is empty. IdentitySuffix string - // Enable Unstable mode for Redis Search module with RESP3. + // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. UnstableResp3 bool } diff --git a/osscluster.go b/osscluster.go index 188f50359e..517fbd4506 100644 --- a/osscluster.go +++ b/osscluster.go @@ -94,6 +94,9 @@ type ClusterOptions struct { DisableIndentity bool // Disable set-lib on connect. Default is false. IdentitySuffix string // Add suffix to client name. Default is empty. + + // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. + UnstableResp3 bool } func (opt *ClusterOptions) init() { @@ -308,7 +311,8 @@ func (opt *ClusterOptions) clientOptions() *Options { // much use for ClusterSlots config). This means we cannot execute the // READONLY command against that node -- setting readOnly to false in such // situations in the options below will prevent that from happening. - readOnly: opt.ReadOnly && opt.ClusterSlots == nil, + readOnly: opt.ReadOnly && opt.ClusterSlots == nil, + UnstableResp3: opt.UnstableResp3, } } diff --git a/universal.go b/universal.go index f4d2d75980..47fda27690 100644 --- a/universal.go +++ b/universal.go @@ -115,6 +115,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, } } diff --git a/universal_test.go b/universal_test.go index 747c68acbd..9328b47764 100644 --- a/universal_test.go +++ b/universal_test.go @@ -38,4 +38,26 @@ var _ = Describe("UniversalClient", func() { }) Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) }) + + It("connect to clusters with UniversalClient and UnstableResp3", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: cluster.addrs(), + Protocol: 3, + UnstableResp3: true, + }) + Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) + a := func() { client.FTInfo(ctx, "all").Result() } + Expect(a).ToNot(Panic()) + }) + + It("connect to clusters with ClusterClient and UnstableResp3", Label("NonRedisEnterprise"), func() { + client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: cluster.addrs(), + Protocol: 3, + UnstableResp3: true, + }) + Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) + a := func() { client.FTInfo(ctx, "all").Result() } + Expect(a).ToNot(Panic()) + }) }) From 01fd453dd087e137bfc0d7a132a24285de31adce Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 12:56:16 +0200 Subject: [PATCH 096/230] fix: flaky ClientKillByFilter test (#3268) --- commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands_test.go b/commands_test.go index 404ffd02be..829a94b654 100644 --- a/commands_test.go +++ b/commands_test.go @@ -217,7 +217,7 @@ var _ = Describe("Commands", func() { killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)), Equal(int64(4)))) + Expect(killed.Val()).To(BeNumerically(">=", 2)) select { case <-done: From 5fdab23102854f14e622ce1d0751e4b9d1ea2399 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:22:43 +0000 Subject: [PATCH 097/230] DOC-4335 added aggregate query examples (#3259) * DOC-4335 added aggregate query examples (demonstrating errors) * DOC-4335 remove incomplete examples * DOC-4335 added missing examples using latest client updates --------- Co-authored-by: Nedyalko Dyakov --- doctests/query_agg_test.go | 433 +++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 doctests/query_agg_test.go diff --git a/doctests/query_agg_test.go b/doctests/query_agg_test.go new file mode 100644 index 0000000000..a710087e48 --- /dev/null +++ b/doctests/query_agg_test.go @@ -0,0 +1,433 @@ +// EXAMPLE: query_agg +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_agg() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START agg1 + res1, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", + "@condition:{new}", + &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{ + { + Field: "@price - (@price * 0.1)", + As: "discounted", + }, + }, + Load: []redis.FTAggregateLoad{ + {Field: "__key"}, + {Field: "price"}, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res1.Rows)) // >>> 5 + + sort.Slice(res1.Rows, func(i, j int) bool { + return res1.Rows[i].Fields["__key"].(string) < + res1.Rows[j].Fields["__key"].(string) + }) + + for _, row := range res1.Rows { + fmt.Printf( + "__key=%v, discounted=%v, price=%v\n", + row.Fields["__key"], + row.Fields["discounted"], + row.Fields["price"], + ) + } + // >>> __key=bicycle:0, discounted=243, price=270 + // >>> __key=bicycle:5, discounted=729, price=810 + // >>> __key=bicycle:6, discounted=2070, price=2300 + // >>> __key=bicycle:7, discounted=387, price=430 + // >>> __key=bicycle:8, discounted=1080, price=1200 + // STEP_END + + // STEP_START agg2 + res2, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{ + {Field: "price"}, + }, + Apply: []redis.FTAggregateApply{ + { + Field: "@price<1000", + As: "price_category", + }, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@condition"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchSum, + Args: []interface{}{"@price_category"}, + As: "num_affordable", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res2.Rows)) // >>> 3 + + sort.Slice(res2.Rows, func(i, j int) bool { + return res2.Rows[i].Fields["condition"].(string) < + res2.Rows[j].Fields["condition"].(string) + }) + + for _, row := range res2.Rows { + fmt.Printf( + "condition=%v, num_affordable=%v\n", + row.Fields["condition"], + row.Fields["num_affordable"], + ) + } + // >>> condition=new, num_affordable=3 + // >>> condition=refurbished, num_affordable=1 + // >>> condition=used, num_affordable=1 + // STEP_END + + // STEP_START agg3 + + res3, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{ + { + Field: "'bicycle'", + As: "type", + }, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@type"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchCount, + As: "num_total", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res3.Rows)) // >>> 1 + + for _, row := range res3.Rows { + fmt.Printf( + "type=%v, num_total=%v\n", + row.Fields["type"], + row.Fields["num_total"], + ) + } + // type=bicycle, num_total=10 + // STEP_END + + // STEP_START agg4 + res4, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{ + {Field: "__key"}, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@condition"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchToList, + Args: []interface{}{"__key"}, + As: "bicycles", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res4.Rows)) // >>> 3 + + sort.Slice(res4.Rows, func(i, j int) bool { + return res4.Rows[i].Fields["condition"].(string) < + res4.Rows[j].Fields["condition"].(string) + }) + + for _, row := range res4.Rows { + rowBikes := row.Fields["bicycles"].([]interface{}) + bikes := make([]string, len(rowBikes)) + + for i, rowBike := range rowBikes { + bikes[i] = rowBike.(string) + } + + sort.Slice(bikes, func(i, j int) bool { + return bikes[i] < bikes[j] + }) + + fmt.Printf( + "condition=%v, bicycles=%v\n", + row.Fields["condition"], + bikes, + ) + } + // >>> condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8] + // >>> condition=refurbished, bicycles=[bicycle:9] + // >>> condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4] + // STEP_END + + // Output: + // 5 + // __key=bicycle:0, discounted=243, price=270 + // __key=bicycle:5, discounted=729, price=810 + // __key=bicycle:6, discounted=2070, price=2300 + // __key=bicycle:7, discounted=387, price=430 + // __key=bicycle:8, discounted=1080, price=1200 + // 3 + // condition=new, num_affordable=3 + // condition=refurbished, num_affordable=1 + // condition=used, num_affordable=1 + // 1 + // type=bicycle, num_total=10 + // 3 + // condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8] + // condition=refurbished, bicycles=[bicycle:9] + // condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4] +} From e59b1856eb599c0c05c506c2dc2d710d34252997 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 10 Feb 2025 14:55:15 +0200 Subject: [PATCH 098/230] test: add test for `info` in RCE 8 (#3269) --- commands_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/commands_test.go b/commands_test.go index 829a94b654..ff48cfce5e 100644 --- a/commands_test.go +++ b/commands_test.go @@ -532,6 +532,59 @@ var _ = Describe("Commands", func() { Expect(info.Val()).To(HaveLen(1)) }) + It("should Info Modules", Label("redis.info"), func() { + SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + info := client.Info(ctx) + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).NotTo(BeNil()) + + info = client.Info(ctx, "search") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + + info = client.Info(ctx, "modules") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + Expect(info.Val()).To(ContainSubstring("ReJSON")) + Expect(info.Val()).To(ContainSubstring("timeseries")) + Expect(info.Val()).To(ContainSubstring("bf")) + + info = client.Info(ctx, "everything") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + Expect(info.Val()).To(ContainSubstring("ReJSON")) + Expect(info.Val()).To(ContainSubstring("timeseries")) + Expect(info.Val()).To(ContainSubstring("bf")) + }) + + It("should InfoMap Modules", Label("redis.info"), func() { + SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + info := client.InfoMap(ctx) + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).NotTo(BeNil()) + + info = client.InfoMap(ctx, "search") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(len(info.Val())).To(BeNumerically(">=", 2)) + Expect(info.Val()["search_version"]).ToNot(BeNil()) + + info = client.InfoMap(ctx, "modules") + Expect(info.Err()).NotTo(HaveOccurred()) + val := info.Val() + modules, ok := val["Modules"] + Expect(ok).To(BeTrue()) + Expect(len(val)).To(BeNumerically(">=", 2)) + Expect(val["search_version"]).ToNot(BeNil()) + Expect(modules["search"]).ToNot(BeNil()) + Expect(modules["ReJSON"]).ToNot(BeNil()) + Expect(modules["timeseries"]).ToNot(BeNil()) + Expect(modules["bf"]).ToNot(BeNil()) + + info = client.InfoMap(ctx, "everything") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(len(info.Val())).To(BeNumerically(">=", 10)) + }) + It("should Info cpu", func() { info := client.Info(ctx, "cpu") Expect(info.Err()).NotTo(HaveOccurred()) From 9a37279edf6646382f898c2682ad182df3ae30be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Mon, 10 Feb 2025 20:55:40 +0800 Subject: [PATCH 099/230] test(redisotel): rename redisotel_test.go to tracing_test.go and add tracing hook tests (#3270) Co-authored-by: Nedyalko Dyakov --- extra/redisotel/go.mod | 6 +- extra/redisotel/go.sum | 4 +- extra/redisotel/redisotel_test.go | 61 -------- extra/redisotel/tracing_test.go | 240 ++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 67 deletions(-) delete mode 100644 extra/redisotel/redisotel_test.go create mode 100644 extra/redisotel/tracing_test.go diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index b2e30b3947..47aab0db14 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -16,13 +16,11 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect golang.org/x/sys v0.16.0 // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/redisotel/go.sum b/extra/redisotel/go.sum index 9eb9bcd4ef..4b832c80f3 100644 --- a/extra/redisotel/go.sum +++ b/extra/redisotel/go.sum @@ -1,7 +1,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/extra/redisotel/redisotel_test.go b/extra/redisotel/redisotel_test.go deleted file mode 100644 index b1ad5ca43a..0000000000 --- a/extra/redisotel/redisotel_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package redisotel - -import ( - "context" - "testing" - - semconv "go.opentelemetry.io/otel/semconv/v1.7.0" - - "go.opentelemetry.io/otel" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" - - "github.com/redis/go-redis/v9" -) - -type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider - -func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider { - return fn(name, opts...) -} - -func TestNewWithTracerProvider(t *testing.T) { - invoked := false - - tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider { - invoked = true - return otel.GetTracerProvider() - }) - - _ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test"))) - - if !invoked { - t.Fatalf("did not call custom TraceProvider") - } -} - -func TestWithDBStatement(t *testing.T) { - provider := sdktrace.NewTracerProvider() - hook := newTracingHook( - "", - WithTracerProvider(provider), - WithDBStatement(false), - ) - ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") - cmd := redis.NewCmd(ctx, "ping") - defer span.End() - - processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { - attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() - for _, attr := range attrs { - if attr.Key == semconv.DBStatementKey { - t.Fatal("Attribute with db statement should not exist") - } - } - return nil - }) - err := processHook(ctx, cmd) - if err != nil { - t.Fatal(err) - } -} diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go new file mode 100644 index 0000000000..bbe8281440 --- /dev/null +++ b/extra/redisotel/tracing_test.go @@ -0,0 +1,240 @@ +package redisotel + +import ( + "context" + "fmt" + "net" + "testing" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" + + "go.opentelemetry.io/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/redis/go-redis/v9" +) + +type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider + +func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider { + return fn(name, opts...) +} + +func TestNewWithTracerProvider(t *testing.T) { + invoked := false + + tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider { + invoked = true + return otel.GetTracerProvider() + }) + + _ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test"))) + + if !invoked { + t.Fatalf("did not call custom TraceProvider") + } +} + +func TestWithDBStatement(t *testing.T) { + provider := sdktrace.NewTracerProvider() + hook := newTracingHook( + "", + WithTracerProvider(provider), + WithDBStatement(false), + ) + ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") + cmd := redis.NewCmd(ctx, "ping") + defer span.End() + + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() + for _, attr := range attrs { + if attr.Key == semconv.DBStatementKey { + t.Fatal("Attribute with db statement should not exist") + } + } + return nil + }) + err := processHook(ctx, cmd) + if err != nil { + t.Fatal(err) + } +} + +func TestTracingHook_DialHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + dialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) { + return nil, tt.errTest + }) + if _, err := dialHook(context.Background(), "tcp", "localhost:6379"); err != tt.errTest { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "redis.dial", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func TestTracingHook_ProcessHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + cmd := redis.NewCmd(context.Background(), "ping") + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + return tt.errTest + }) + assertEqual(t, tt.errTest, processHook(context.Background(), cmd)) + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "ping", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func TestTracingHook_ProcessPipelineHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + cmds := []redis.Cmder{ + redis.NewCmd(context.Background(), "ping"), + redis.NewCmd(context.Background(), "ping"), + } + processHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error { + return tt.errTest + }) + assertEqual(t, tt.errTest, processHook(context.Background(), cmds)) + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "redis.pipeline ping", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping\nping")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func assertEqual(t *testing.T, expected, actual interface{}) { + t.Helper() + if expected != actual { + t.Fatalf("expected %v, got %v", expected, actual) + } +} + +func assertAttributeContains(t *testing.T, attrs []attribute.KeyValue, attr attribute.KeyValue) { + t.Helper() + for _, a := range attrs { + if a == attr { + return + } + } + t.Fatalf("attribute %v not found", attr) +} From f83f92242be962f88b7a28ec845e9610f1582d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Mon, 10 Feb 2025 21:03:50 +0800 Subject: [PATCH 100/230] chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 (#3265) * chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 * chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 --- example/del-keys-without-ttl/go.mod | 2 +- example/del-keys-without-ttl/go.sum | 4 ++-- example/hll/go.mod | 2 +- example/hll/go.sum | 4 ++-- example/lua-scripting/go.mod | 2 +- example/lua-scripting/go.sum | 4 ++-- example/otel/go.mod | 2 +- example/otel/go.sum | 4 ++-- example/redis-bloom/go.mod | 2 +- example/redis-bloom/go.sum | 4 ++-- example/scan-struct/go.mod | 2 +- example/scan-struct/go.sum | 4 ++-- extra/rediscensus/go.mod | 6 ++---- extra/rediscensus/go.sum | 4 ++-- extra/rediscmd/go.mod | 6 ++---- extra/rediscmd/go.sum | 4 ++-- extra/redisprometheus/go.mod | 6 ++---- extra/redisprometheus/go.sum | 4 ++-- 18 files changed, 30 insertions(+), 36 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index d725db0bbb..9290eb0393 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -10,7 +10,7 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect diff --git a/example/del-keys-without-ttl/go.sum b/example/del-keys-without-ttl/go.sum index e426a762bb..96beed56e1 100644 --- a/example/del-keys-without-ttl/go.sum +++ b/example/del-keys-without-ttl/go.sum @@ -1,8 +1,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/hll/go.mod b/example/hll/go.mod index 7093be4202..e2bf03d922 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/hll/go.sum b/example/hll/go.sum index 0e92df5e78..d64ea0303f 100644 --- a/example/hll/go.sum +++ b/example/hll/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 85a82860a5..5c811bf255 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/lua-scripting/go.sum b/example/lua-scripting/go.sum index 0e92df5e78..d64ea0303f 100644 --- a/example/lua-scripting/go.sum +++ b/example/lua-scripting/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/otel/go.mod b/example/otel/go.mod index 3f1d858e17..f5e2a156cb 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -17,7 +17,7 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index e85481dbeb..1a1729c6ef 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -2,8 +2,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 3825432a7d..9076e14740 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/redis-bloom/go.sum b/example/redis-bloom/go.sum index 0e92df5e78..d64ea0303f 100644 --- a/example/redis-bloom/go.sum +++ b/example/redis-bloom/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index fca1a59720..f14f54df1f 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -10,6 +10,6 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/scan-struct/go.sum b/example/scan-struct/go.sum index 6274a65f69..5496d29e58 100644 --- a/example/scan-struct/go.sum +++ b/example/scan-struct/go.sum @@ -1,7 +1,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index bae3f7b939..a28ad7dfdc 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -13,11 +13,9 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/rediscensus/go.sum b/extra/rediscensus/go.sum index cf8f907216..ab3a8984fa 100644 --- a/extra/rediscensus/go.sum +++ b/extra/rediscensus/go.sum @@ -3,8 +3,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 594cfdf1e1..07df0cc2df 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -11,10 +11,8 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/rediscmd/go.sum b/extra/rediscmd/go.sum index 21b4f64ee2..4db68f6d4f 100644 --- a/extra/rediscmd/go.sum +++ b/extra/rediscmd/go.sum @@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 5cbafac11a..42a6f805c2 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -11,7 +11,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -22,6 +22,4 @@ require ( google.golang.org/protobuf v1.33.0 // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/redisprometheus/go.sum b/extra/redisprometheus/go.sum index 6528a5e331..7093016eec 100644 --- a/extra/redisprometheus/go.sum +++ b/extra/redisprometheus/go.sum @@ -2,8 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= From 18bc8edbcf07ded345615e85b3638843a0997148 Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Tue, 11 Feb 2025 07:50:31 -0800 Subject: [PATCH 101/230] Reinstate read-only lock on hooks access in dialHook (#3225) --- redis.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/redis.go b/redis.go index 2f576bdbe3..ec3ff616ac 100644 --- a/redis.go +++ b/redis.go @@ -41,7 +41,7 @@ type ( ) type hooksMixin struct { - hooksMu *sync.Mutex + hooksMu *sync.RWMutex slice []Hook initial hooks @@ -49,7 +49,7 @@ type hooksMixin struct { } func (hs *hooksMixin) initHooks(hooks hooks) { - hs.hooksMu = new(sync.Mutex) + hs.hooksMu = new(sync.RWMutex) hs.initial = hooks hs.chain() } @@ -151,7 +151,7 @@ func (hs *hooksMixin) clone() hooksMixin { clone := *hs l := len(clone.slice) clone.slice = clone.slice[:l:l] - clone.hooksMu = new(sync.Mutex) + clone.hooksMu = new(sync.RWMutex) return clone } @@ -176,7 +176,14 @@ func (hs *hooksMixin) withProcessPipelineHook( } func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) { - return hs.current.dial(ctx, network, addr) + // Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...) + // while this dialer is concurrently accessed by the background connection pool population + // routine when MinIdleConns > 0. + hs.hooksMu.RLock() + current := hs.current + hs.hooksMu.RUnlock() + + return current.dial(ctx, network, addr) } func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error { From 7b400169e3509b5cdeeb7315cf6999374ef91160 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 14 Feb 2025 13:07:39 +0200 Subject: [PATCH 102/230] use limit when limitoffset is zero (#3275) --- search_commands.go | 14 ++++---------- search_test.go | 5 +++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/search_commands.go b/search_commands.go index df12bb3f97..878f874ef4 100644 --- a/search_commands.go +++ b/search_commands.go @@ -574,11 +574,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.SortByMax > 0 { queryArgs = append(queryArgs, "MAX", options.SortByMax) } - if options.LimitOffset > 0 { - queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) - } - if options.Limit > 0 { - queryArgs = append(queryArgs, options.Limit) + if options.LimitOffset >= 0 && options.Limit > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) } if options.Filter != "" { queryArgs = append(queryArgs, "FILTER", options.Filter) @@ -773,11 +770,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.SortByMax > 0 { args = append(args, "MAX", options.SortByMax) } - if options.LimitOffset > 0 { - args = append(args, "LIMIT", options.LimitOffset) - } - if options.Limit > 0 { - args = append(args, options.Limit) + if options.LimitOffset >= 0 && options.Limit > 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) } if options.Filter != "" { args = append(args, "FILTER", options.Filter) diff --git a/search_test.go b/search_test.go index e4e5521526..993116da93 100644 --- a/search_test.go +++ b/search_test.go @@ -616,6 +616,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) + + options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 0} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) }) It("should FTAggregate load ", Label("search", "ftaggregate"), func() { From 61391aac2d3f1582938f5ee90e0ad8b528a81d32 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 18 Feb 2025 13:51:22 +0200 Subject: [PATCH 103/230] fix: linter configuration (#3279) --- .golangci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index de514554a9..285aca6b3a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,3 @@ run: - concurrency: 8 - deadline: 5m + timeout: 5m tests: false From 82be3f4cbe1ce51751aa40c6d834f38a4269c884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:04:03 +0200 Subject: [PATCH 104/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3276) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.46.0 to 0.47.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.46.0...0.47.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 95cfdfaa04..beefa6164f 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.46.0 + uses: rojopolis/spellcheck-github-actions@0.47.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 1814bae1fd2cd8d7ec94d24febdcd0eef69e5c13 Mon Sep 17 00:00:00 2001 From: herodot <54836727+bitsark@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:55:49 +0800 Subject: [PATCH 105/230] fix(search&aggregate):fix error overwrite and typo #3220 (#3224) * fix (#3220) * LOAD has NO AS param(https://redis.io/docs/latest/commands/ft.aggregate/) * fix typo: WITHCOUT -> WITHCOUNT * fix (#3220): * Compatible with known RediSearch issue in test * fix (#3220) * fixed the calculation bug of the count of load params * test should not include special condition * return errors when they occur --------- Co-authored-by: Nedyalko Dyakov Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- search_commands.go | 22 +++++++++++++--------- search_test.go | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/search_commands.go b/search_commands.go index 878f874ef4..71ee6ab32e 100644 --- a/search_commands.go +++ b/search_commands.go @@ -514,12 +514,16 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } if options.Load != nil { queryArgs = append(queryArgs, "LOAD", len(options.Load)) + index, count := len(queryArgs)-1, 0 for _, load := range options.Load { queryArgs = append(queryArgs, load.Field) + count++ if load.As != "" { queryArgs = append(queryArgs, "AS", load.As) + count += 2 } } + queryArgs[index] = count } if options.Timeout > 0 { @@ -677,12 +681,10 @@ func (cmd *AggregateCmd) String() string { func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err return err } cmd.val, err = ProcessAggregateResult(data) if err != nil { - cmd.err = err return err } return nil @@ -713,12 +715,16 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st } if options.Load != nil { args = append(args, "LOAD", len(options.Load)) + index, count := len(args)-1, 0 for _, load := range options.Load { args = append(args, load.Field) + count++ if load.As != "" { args = append(args, "AS", load.As) + count += 2 } } + args[index] = count } if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) @@ -1420,7 +1426,7 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { } cmd.val, err = parseFTInfo(data) if err != nil { - cmd.err = err + return err } return nil @@ -1513,12 +1519,11 @@ func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) { func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err - return nil + return err } cmd.val, err = parseFTSpellCheck(data) if err != nil { - cmd.err = err + return err } return nil } @@ -1702,12 +1707,11 @@ func (cmd *FTSearchCmd) RawResult() (interface{}, error) { func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err - return nil + return err } cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) if err != nil { - cmd.err = err + return err } return nil } diff --git a/search_test.go b/search_test.go index 993116da93..ea3460d3d3 100644 --- a/search_test.go +++ b/search_test.go @@ -269,6 +269,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(BeEquivalentTo(int64(1))) + _, err = client.FTSearch(ctx, "idx_not_exist", "only in the body").Result() + Expect(err).To(HaveOccurred()) }) It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() { @@ -643,11 +645,25 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2", As: "t2alias"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world")) + + options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}, {Field: "t2", As: "t2alias"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) + Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world")) + options = &redis.FTAggregateOptions{LoadAll: true} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + + _, err = client.FTAggregateWithArgs(ctx, "idx_not_exist", "*", &redis.FTAggregateOptions{}).Result() + Expect(err).To(HaveOccurred()) }) It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { @@ -1268,6 +1284,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(Equal("OK")) + WaitForIndexing(client, "idx_hash") ftSearchOptions := &redis.FTSearchOptions{ DialectVersion: 4, From 926d6f16775156c3814ae96b69bb699344484fe9 Mon Sep 17 00:00:00 2001 From: alingse Date: Thu, 20 Feb 2025 22:22:34 +0800 Subject: [PATCH 106/230] move regexp.MustCompile close to call (#3280) * move regexp.MustCompile out of func * move moduleRe close to call --------- Co-authored-by: Nedyalko Dyakov --- command.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/command.go b/command.go index 2623a23960..696501453a 100644 --- a/command.go +++ b/command.go @@ -5492,8 +5492,6 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error { section := "" scanner := bufio.NewScanner(strings.NewReader(val)) - moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`) - for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") { @@ -5504,6 +5502,7 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error { cmd.val[section] = make(map[string]string) } else if line != "" { if section == "Modules" { + moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`) kv := moduleRe.FindStringSubmatch(line) if len(kv) == 3 { cmd.val[section][kv[1]] = kv[2] From e010f2e2ff9abe0c7dff49a614a0279e6850ad3d Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:23:05 +0000 Subject: [PATCH 107/230] DOC-4329 added range query examples (#3252) Co-authored-by: Nedyalko Dyakov --- doctests/query_range_test.go | 376 +++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 doctests/query_range_test.go diff --git a/doctests/query_range_test.go b/doctests/query_range_test.go new file mode 100644 index 0000000000..41438ff0e5 --- /dev/null +++ b/doctests/query_range_test.go @@ -0,0 +1,376 @@ +// EXAMPLE: query_range +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_range() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START range1 + res1, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "@price:[500 1000]", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 3 + + for _, doc := range res1.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 + // STEP_END + + // STEP_START range2 + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTSearchOptions{ + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: 500, + Max: 1000, + }, + }, + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 3 + + for _, doc := range res2.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 + // STEP_END + + // STEP_START range3 + res3, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: "(1000", + Max: "+inf", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + for _, doc := range res3.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:1 : price 1200 + // >>> bicycle:4 : price 3200 + // >>> bicycle:6 : price 2300 + // >>> bicycle:3 : price 3400 + // >>> bicycle:8 : price 1200 + // STEP_END + + // STEP_START range4 + res4, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@price:[-inf 2000]", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + SortBy: []redis.FTSearchSortBy{ + { + FieldName: "price", + Asc: true, + }, + }, + LimitOffset: 0, + Limit: 5, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4.Total) // >>> 7 + + for _, doc := range res4.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:0 : price 270 + // >>> bicycle:7 : price 430 + // >>> bicycle:5 : price 810 + // >>> bicycle:2 : price 815 + // >>> bicycle:9 : price 815 + // STEP_END + + // Output: + // 3 + // bicycle:2 : price 815 + // bicycle:5 : price 810 + // bicycle:9 : price 815 + // 3 + // bicycle:2 : price 815 + // bicycle:5 : price 810 + // bicycle:9 : price 815 + // 5 + // bicycle:1 : price 1200 + // bicycle:4 : price 3200 + // bicycle:6 : price 2300 + // bicycle:3 : price 3400 + // bicycle:8 : price 1200 + // 7 + // bicycle:0 : price 270 + // bicycle:7 : price 430 + // bicycle:5 : price 810 + // bicycle:2 : price 815 + // bicycle:9 : price 815 +} From 26e2430865d2ab5b8aa86d52444574dc02eed916 Mon Sep 17 00:00:00 2001 From: Ali Error Date: Thu, 20 Feb 2025 17:54:11 +0300 Subject: [PATCH 108/230] fix: nil pointer dereferencing in writeArg (#3271) * fixed bug with nil dereferencing in writeArg, added hset struct example, added tests * removed password from example * added omitempty * reverted xxhash versioning * reverted xxhash versioning * removed password * removed password --------- Co-authored-by: Nedyalko Dyakov --- example/hset-struct/README.md | 7 ++ example/hset-struct/go.mod | 15 ++++ example/hset-struct/go.sum | 10 +++ example/hset-struct/main.go | 129 ++++++++++++++++++++++++++++++++++ example/scan-struct/main.go | 6 ++ internal/proto/writer.go | 53 ++++++++++++++ internal/proto/writer_test.go | 83 ++++++++++++++-------- 7 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 example/hset-struct/README.md create mode 100644 example/hset-struct/go.mod create mode 100644 example/hset-struct/go.sum create mode 100644 example/hset-struct/main.go diff --git a/example/hset-struct/README.md b/example/hset-struct/README.md new file mode 100644 index 0000000000..e6cb4523c3 --- /dev/null +++ b/example/hset-struct/README.md @@ -0,0 +1,7 @@ +# Example for setting struct fields as hash fields + +To run this example: + +```shell +go run . +``` diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod new file mode 100644 index 0000000000..fca1a59720 --- /dev/null +++ b/example/hset-struct/go.mod @@ -0,0 +1,15 @@ +module github.com/redis/go-redis/example/scan-struct + +go 1.18 + +replace github.com/redis/go-redis/v9 => ../.. + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/redis/go-redis/v9 v9.6.2 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) diff --git a/example/hset-struct/go.sum b/example/hset-struct/go.sum new file mode 100644 index 0000000000..1602e702e5 --- /dev/null +++ b/example/hset-struct/go.sum @@ -0,0 +1,10 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/hset-struct/main.go b/example/hset-struct/main.go new file mode 100644 index 0000000000..2e08f542fb --- /dev/null +++ b/example/hset-struct/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "time" + + "github.com/davecgh/go-spew/spew" + + "github.com/redis/go-redis/v9" +) + +type Model struct { + Str1 string `redis:"str1"` + Str2 string `redis:"str2"` + Str3 *string `redis:"str3"` + Str4 *string `redis:"str4"` + Bytes []byte `redis:"bytes"` + Int int `redis:"int"` + Int2 *int `redis:"int2"` + Int3 *int `redis:"int3"` + Bool bool `redis:"bool"` + Bool2 *bool `redis:"bool2"` + Bool3 *bool `redis:"bool3"` + Bool4 *bool `redis:"bool4,omitempty"` + Time time.Time `redis:"time"` + Time2 *time.Time `redis:"time2"` + Time3 *time.Time `redis:"time3"` + Ignored struct{} `redis:"-"` +} + +func main() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: ":6379", + }) + + _ = rdb.FlushDB(ctx).Err() + + t := time.Date(2025, 02, 8, 0, 0, 0, 0, time.UTC) + + data := Model{ + Str1: "hello", + Str2: "world", + Str3: ToPtr("hello"), + Str4: nil, + Bytes: []byte("this is bytes !"), + Int: 123, + Int2: ToPtr(0), + Int3: nil, + Bool: true, + Bool2: ToPtr(false), + Bool3: nil, + Time: t, + Time2: ToPtr(t), + Time3: nil, + Ignored: struct{}{}, + } + + // Set some fields. + if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { + rdb.HMSet(ctx, "key", data) + return nil + }); err != nil { + panic(err) + } + + var model1, model2 Model + + // Scan all fields into the model. + if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { + panic(err) + } + + // Or scan a subset of the fields. + if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { + panic(err) + } + + spew.Dump(model1) + // Output: + // (main.Model) { + // Str1: (string) (len=5) "hello", + // Str2: (string) (len=5) "world", + // Str3: (*string)(0xc000016970)((len=5) "hello"), + // Str4: (*string)(0xc000016980)(""), + // Bytes: ([]uint8) (len=15 cap=16) { + // 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !| + // }, + // Int: (int) 123, + // Int2: (*int)(0xc000014568)(0), + // Int3: (*int)(0xc000014560)(0), + // Bool: (bool) true, + // Bool2: (*bool)(0xc000014570)(false), + // Bool3: (*bool)(0xc000014548)(false), + // Bool4: (*bool)(), + // Time: (time.Time) 2025-02-08 00:00:00 +0000 UTC, + // Time2: (*time.Time)(0xc0000122a0)(2025-02-08 00:00:00 +0000 UTC), + // Time3: (*time.Time)(0xc000012288)(0001-01-01 00:00:00 +0000 UTC), + // Ignored: (struct {}) { + // } + // } + + spew.Dump(model2) + // Output: + // (main.Model) { + // Str1: (string) (len=5) "hello", + // Str2: (string) "", + // Str3: (*string)(), + // Str4: (*string)(), + // Bytes: ([]uint8) , + // Int: (int) 123, + // Int2: (*int)(), + // Int3: (*int)(), + // Bool: (bool) false, + // Bool2: (*bool)(), + // Bool3: (*bool)(), + // Bool4: (*bool)(), + // Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + // Time2: (*time.Time)(), + // Time3: (*time.Time)(), + // Ignored: (struct {}) { + // } + // } +} + +func ToPtr[T any](v T) *T { + return &v +} diff --git a/example/scan-struct/main.go b/example/scan-struct/main.go index cc877b8478..2dc5b85c18 100644 --- a/example/scan-struct/main.go +++ b/example/scan-struct/main.go @@ -11,9 +11,12 @@ import ( type Model struct { Str1 string `redis:"str1"` Str2 string `redis:"str2"` + Str3 *string `redis:"str3"` Bytes []byte `redis:"bytes"` Int int `redis:"int"` + Int2 *int `redis:"int2"` Bool bool `redis:"bool"` + Bool2 *bool `redis:"bool2"` Ignored struct{} `redis:"-"` } @@ -29,8 +32,11 @@ func main() { if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { rdb.HSet(ctx, "key", "str1", "hello") rdb.HSet(ctx, "key", "str2", "world") + rdb.HSet(ctx, "key", "str3", "") rdb.HSet(ctx, "key", "int", 123) + rdb.HSet(ctx, "key", "int2", 0) rdb.HSet(ctx, "key", "bool", 1) + rdb.HSet(ctx, "key", "bool2", 0) rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !")) return nil }); err != nil { diff --git a/internal/proto/writer.go b/internal/proto/writer.go index 78595cc4f0..38e66c6887 100644 --- a/internal/proto/writer.go +++ b/internal/proto/writer.go @@ -66,56 +66,95 @@ func (w *Writer) WriteArg(v interface{}) error { case string: return w.string(v) case *string: + if v == nil { + return w.string("") + } return w.string(*v) case []byte: return w.bytes(v) case int: return w.int(int64(v)) case *int: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int8: return w.int(int64(v)) case *int8: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int16: return w.int(int64(v)) case *int16: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int32: return w.int(int64(v)) case *int32: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int64: return w.int(v) case *int64: + if v == nil { + return w.int(0) + } return w.int(*v) case uint: return w.uint(uint64(v)) case *uint: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint8: return w.uint(uint64(v)) case *uint8: + if v == nil { + return w.string("") + } return w.uint(uint64(*v)) case uint16: return w.uint(uint64(v)) case *uint16: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint32: return w.uint(uint64(v)) case *uint32: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint64: return w.uint(v) case *uint64: + if v == nil { + return w.uint(0) + } return w.uint(*v) case float32: return w.float(float64(v)) case *float32: + if v == nil { + return w.float(0) + } return w.float(float64(*v)) case float64: return w.float(v) case *float64: + if v == nil { + return w.float(0) + } return w.float(*v) case bool: if v { @@ -123,6 +162,9 @@ func (w *Writer) WriteArg(v interface{}) error { } return w.int(0) case *bool: + if v == nil { + return w.int(0) + } if *v { return w.int(1) } @@ -130,8 +172,19 @@ func (w *Writer) WriteArg(v interface{}) error { case time.Time: w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano) return w.bytes(w.numBuf) + case *time.Time: + if v == nil { + v = &time.Time{} + } + w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano) + return w.bytes(w.numBuf) case time.Duration: return w.int(v.Nanoseconds()) + case *time.Duration: + if v == nil { + return w.int(0) + } + return w.int(v.Nanoseconds()) case encoding.BinaryMarshaler: b, err := v.MarshalBinary() if err != nil { diff --git a/internal/proto/writer_test.go b/internal/proto/writer_test.go index 7c9d20884a..1d5152dc08 100644 --- a/internal/proto/writer_test.go +++ b/internal/proto/writer_test.go @@ -111,36 +111,61 @@ var _ = Describe("WriteArg", func() { wr = proto.NewWriter(buf) }) + t := time.Date(2025, 2, 8, 00, 00, 00, 0, time.UTC) + args := map[any]string{ - "hello": "$5\r\nhello\r\n", - int(10): "$2\r\n10\r\n", - util.ToPtr(int(10)): "$2\r\n10\r\n", - int8(10): "$2\r\n10\r\n", - util.ToPtr(int8(10)): "$2\r\n10\r\n", - int16(10): "$2\r\n10\r\n", - util.ToPtr(int16(10)): "$2\r\n10\r\n", - int32(10): "$2\r\n10\r\n", - util.ToPtr(int32(10)): "$2\r\n10\r\n", - int64(10): "$2\r\n10\r\n", - util.ToPtr(int64(10)): "$2\r\n10\r\n", - uint(10): "$2\r\n10\r\n", - util.ToPtr(uint(10)): "$2\r\n10\r\n", - uint8(10): "$2\r\n10\r\n", - util.ToPtr(uint8(10)): "$2\r\n10\r\n", - uint16(10): "$2\r\n10\r\n", - util.ToPtr(uint16(10)): "$2\r\n10\r\n", - uint32(10): "$2\r\n10\r\n", - util.ToPtr(uint32(10)): "$2\r\n10\r\n", - uint64(10): "$2\r\n10\r\n", - util.ToPtr(uint64(10)): "$2\r\n10\r\n", - float32(10.3): "$18\r\n10.300000190734863\r\n", - util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n", - float64(10.3): "$4\r\n10.3\r\n", - util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n", - bool(true): "$1\r\n1\r\n", - bool(false): "$1\r\n0\r\n", - util.ToPtr(bool(true)): "$1\r\n1\r\n", - util.ToPtr(bool(false)): "$1\r\n0\r\n", + "hello": "$5\r\nhello\r\n", + util.ToPtr("hello"): "$5\r\nhello\r\n", + (*string)(nil): "$0\r\n\r\n", + int(10): "$2\r\n10\r\n", + util.ToPtr(int(10)): "$2\r\n10\r\n", + (*int)(nil): "$1\r\n0\r\n", + int8(10): "$2\r\n10\r\n", + util.ToPtr(int8(10)): "$2\r\n10\r\n", + (*int8)(nil): "$1\r\n0\r\n", + int16(10): "$2\r\n10\r\n", + util.ToPtr(int16(10)): "$2\r\n10\r\n", + (*int16)(nil): "$1\r\n0\r\n", + int32(10): "$2\r\n10\r\n", + util.ToPtr(int32(10)): "$2\r\n10\r\n", + (*int32)(nil): "$1\r\n0\r\n", + int64(10): "$2\r\n10\r\n", + util.ToPtr(int64(10)): "$2\r\n10\r\n", + (*int64)(nil): "$1\r\n0\r\n", + uint(10): "$2\r\n10\r\n", + util.ToPtr(uint(10)): "$2\r\n10\r\n", + (*uint)(nil): "$1\r\n0\r\n", + uint8(10): "$2\r\n10\r\n", + util.ToPtr(uint8(10)): "$2\r\n10\r\n", + (*uint8)(nil): "$0\r\n\r\n", + uint16(10): "$2\r\n10\r\n", + util.ToPtr(uint16(10)): "$2\r\n10\r\n", + (*uint16)(nil): "$1\r\n0\r\n", + uint32(10): "$2\r\n10\r\n", + util.ToPtr(uint32(10)): "$2\r\n10\r\n", + (*uint32)(nil): "$1\r\n0\r\n", + uint64(10): "$2\r\n10\r\n", + util.ToPtr(uint64(10)): "$2\r\n10\r\n", + (*uint64)(nil): "$1\r\n0\r\n", + float32(10.3): "$18\r\n10.300000190734863\r\n", + util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n", + (*float32)(nil): "$1\r\n0\r\n", + float64(10.3): "$4\r\n10.3\r\n", + util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n", + (*float64)(nil): "$1\r\n0\r\n", + bool(true): "$1\r\n1\r\n", + bool(false): "$1\r\n0\r\n", + util.ToPtr(bool(true)): "$1\r\n1\r\n", + util.ToPtr(bool(false)): "$1\r\n0\r\n", + (*bool)(nil): "$1\r\n0\r\n", + time.Time(t): "$20\r\n2025-02-08T00:00:00Z\r\n", + util.ToPtr(time.Time(t)): "$20\r\n2025-02-08T00:00:00Z\r\n", + (*time.Time)(nil): "$20\r\n0001-01-01T00:00:00Z\r\n", + time.Duration(time.Second): "$10\r\n1000000000\r\n", + util.ToPtr(time.Duration(time.Second)): "$10\r\n1000000000\r\n", + (*time.Duration)(nil): "$1\r\n0\r\n", + (encoding.BinaryMarshaler)(&MyType{}): "$5\r\nhello\r\n", + (encoding.BinaryMarshaler)(nil): "$0\r\n\r\n", } for arg, expect := range args { From 4f9550fd9989ce2fc89af0afeca6ee407bcc69aa Mon Sep 17 00:00:00 2001 From: "fengyun.rui" Date: Thu, 20 Feb 2025 22:55:54 +0800 Subject: [PATCH 109/230] fix: race slice for list function of ring client (#2931) * fix: race slice for list of ring client Signed-off-by: rfyiamcool * fix: copy wrong list Co-authored-by: Nedyalko Dyakov --------- Signed-off-by: rfyiamcool Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- ring.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ring.go b/ring.go index b402217344..06a26020a8 100644 --- a/ring.go +++ b/ring.go @@ -341,7 +341,8 @@ func (c *ringSharding) List() []*ringShard { c.mu.RLock() if !c.closed { - list = c.shards.list + list = make([]*ringShard, len(c.shards.list)) + copy(list, c.shards.list) } c.mu.RUnlock() From 0bd9d4a8ee95d28d6ecbeadd08a501a0ee2100c8 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 24 Feb 2025 12:45:08 +0200 Subject: [PATCH 110/230] V9.7.1 -> master (#3287) --- .github/workflows/build.yml | 4 ++-- .github/workflows/golangci-lint.yml | 6 ++---- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- search_test.go | 1 - version.go | 2 +- 14 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5852fcde43..afec49d8a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9] + branches: [master, v9, v9.7] pull_request: - branches: [master, v9] + branches: [master, v9, v9.7] permissions: contents: read diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5210ccfa23..d9e53f706e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -12,15 +12,13 @@ on: permissions: contents: read + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests jobs: golangci: - permissions: - contents: read # for actions/checkout to fetch code - pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v6.5.0 diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 9290eb0393..40ad6297fb 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index e2bf03d922..14a8827fd1 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 5c811bf255..64f5c8af15 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index f5e2a156cb..93b5d46c25 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 9076e14740..a973cd1790 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index f14f54df1f..21d7e527d7 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index a28ad7dfdc..cc0bd0fb5f 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 07df0cc2df..0689fe904d 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 47aab0db14..ab6288dec2 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 42a6f805c2..a1659bb0f6 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/search_test.go b/search_test.go index ea3460d3d3..e08ce3d39a 100644 --- a/search_test.go +++ b/search_test.go @@ -667,7 +667,6 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { - SkipBeforeRedisMajor(8, "ADDSCORES is available in Redis CE 8") title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false} description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result() diff --git a/version.go b/version.go index 7cb060b5d7..a447a546de 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.6.2" + return "9.7.1" } From 0a76a3570b0f2b0d9f219ecce6aa126aa207bfc8 Mon Sep 17 00:00:00 2001 From: Tom Bayes Date: Thu, 27 Feb 2025 00:39:45 +0900 Subject: [PATCH 111/230] Add test codes for search_commands.go (#3285) * feat: add test codes for search_commands.go * feat: move ftaggregate tests to search_test.go * Update search_test.go Co-authored-by: Nedyalko Dyakov * feat: remove reflect from test * Update search_test.go fix type in Sprintf --------- Co-authored-by: Nedyalko Dyakov --- .gitignore | 1 + search_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/.gitignore b/.gitignore index f1883206a7..a02e552172 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ testdata/* *.tar.gz *.dic redis8tests.sh +.vscode diff --git a/search_test.go b/search_test.go index e08ce3d39a..1098319e05 100644 --- a/search_test.go +++ b/search_test.go @@ -805,6 +805,73 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { } }) + It("should return only the base query when options is nil", Label("search", "ftaggregate"), func() { + args := redis.FTAggregateQuery("testQuery", nil) + Expect(args).To(Equal(redis.AggregateQuery{"testQuery"})) + }) + + It("should include VERBATIM and SCORER when options are set", Label("search", "ftaggregate"), func() { + options := &redis.FTAggregateOptions{ + Verbatim: true, + Scorer: "BM25", + } + args := redis.FTAggregateQuery("testQuery", options) + Expect(args[0]).To(Equal("testQuery")) + Expect(args).To(ContainElement("VERBATIM")) + Expect(args).To(ContainElement("SCORER")) + Expect(args).To(ContainElement("BM25")) + }) + + It("should include ADDSCORES when AddScores is true", Label("search", "ftaggregate"), func() { + options := &redis.FTAggregateOptions{ + AddScores: true, + } + args := redis.FTAggregateQuery("q", options) + Expect(args).To(ContainElement("ADDSCORES")) + }) + + It("should include LOADALL when LoadAll is true", Label("search", "ftaggregate"), func() { + options := &redis.FTAggregateOptions{ + LoadAll: true, + } + args := redis.FTAggregateQuery("q", options) + Expect(args).To(ContainElement("LOAD")) + Expect(args).To(ContainElement("*")) + }) + + It("should include LOAD when Load is provided", Label("search", "ftaggregate"), func() { + options := &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{ + {Field: "field1", As: "alias1"}, + {Field: "field2"}, + }, + } + args := redis.FTAggregateQuery("q", options) + // Verify LOAD options related arguments + Expect(args).To(ContainElement("LOAD")) + // Check that field names and aliases are present + Expect(args).To(ContainElement("field1")) + Expect(args).To(ContainElement("alias1")) + Expect(args).To(ContainElement("field2")) + }) + + It("should include TIMEOUT when Timeout > 0", Label("search", "ftaggregate"), func() { + options := &redis.FTAggregateOptions{ + Timeout: 500, + } + args := redis.FTAggregateQuery("q", options) + Expect(args).To(ContainElement("TIMEOUT")) + found := false + for i, a := range args { + if fmt.Sprintf("%s", a) == "TIMEOUT" { + Expect(fmt.Sprintf("%d", args[i+1])).To(Equal("500")) + found = true + break + } + } + Expect(found).To(BeTrue()) + }) + It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() { client.HSet(ctx, "doc1", "foo", "bar") From eaaac08f98e99abc9d7aa1e798ac055f14523c5b Mon Sep 17 00:00:00 2001 From: Rushikesh Joshi Date: Wed, 26 Feb 2025 08:25:55 -0800 Subject: [PATCH 112/230] feat: support Elasticache cluster mode by introducing IsClusterMode config param (#3255) Co-authored-by: Rushikesh Joshi Co-authored-by: Nedyalko Dyakov --- universal.go | 5 ++++- universal_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/universal.go b/universal.go index 47fda27690..21867ec224 100644 --- a/universal.go +++ b/universal.go @@ -69,6 +69,9 @@ type UniversalOptions struct { DisableIndentity bool IdentitySuffix string UnstableResp3 bool + + // IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint). + IsClusterMode bool } // Cluster returns cluster options created from the universal options. @@ -244,7 +247,7 @@ var ( func NewUniversalClient(opts *UniversalOptions) UniversalClient { if opts.MasterName != "" { return NewFailoverClient(opts.Failover()) - } else if len(opts.Addrs) > 1 { + } else if len(opts.Addrs) > 1 || opts.IsClusterMode { return NewClusterClient(opts.Cluster()) } return NewClient(opts.Simple()) diff --git a/universal_test.go b/universal_test.go index 9328b47764..ba04324f43 100644 --- a/universal_test.go +++ b/universal_test.go @@ -60,4 +60,21 @@ var _ = Describe("UniversalClient", func() { a := func() { client.FTInfo(ctx, "all").Result() } Expect(a).ToNot(Panic()) }) + + It("should connect to clusters if IsClusterMode is set even if only a single address is provided", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{cluster.addrs()[0]}, + IsClusterMode: true, + }) + _, ok := client.(*redis.ClusterClient) + Expect(ok).To(BeTrue(), "expected a ClusterClient") + }) + + It("should return all slots after instantiating UniversalClient with IsClusterMode", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{cluster.addrs()[0]}, + IsClusterMode: true, + }) + Expect(client.ClusterSlots(ctx).Val()).To(HaveLen(3)) + }) }) From 4d075dd648f8749a04a64f5b436082c58b0fb0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Thu, 27 Feb 2025 23:14:46 +0800 Subject: [PATCH 113/230] fix(redisotel): fix the situation of reporting spans multiple times (#3168) Co-authored-by: Nedyalko Dyakov --- extra/redisotel/tracing.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/extra/redisotel/tracing.go b/extra/redisotel/tracing.go index 3d5f3426ca..33b7abac18 100644 --- a/extra/redisotel/tracing.go +++ b/extra/redisotel/tracing.go @@ -30,8 +30,6 @@ func InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error { rdb.AddHook(newTracingHook(connString, opts...)) return nil case *redis.ClusterClient: - rdb.AddHook(newTracingHook("", opts...)) - rdb.OnNewNode(func(rdb *redis.Client) { opt := rdb.Options() opts = addServerAttributes(opts, opt.Addr) @@ -40,8 +38,6 @@ func InstrumentTracing(rdb redis.UniversalClient, opts ...TracingOption) error { }) return nil case *redis.Ring: - rdb.AddHook(newTracingHook("", opts...)) - rdb.OnNewNode(func(rdb *redis.Client) { opt := rdb.Options() opts = addServerAttributes(opts, opt.Addr) From 4451928608593396a95c310f4c6d05086bd98ece Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 28 Feb 2025 12:49:00 +0200 Subject: [PATCH 114/230] feat: Enable CI for Redis CE 8.0 (#3274) * chore: extract benchmark tests * wip * enable pubsub tests * enable ring tests * stop tests with build redis from source * start all tests * mix of makefile and action * add sentinel configs * fix example test * stop debug on re * wip * enable gears for redis 7.2 * wip * enable sentinel, they are expected to fail * fix: linter configuration * chore: update re versions * return older redis enterprise version * add basic codeql * wip: increase timeout, focus only sentinel tests * sentinels with docker network host * enable all tests * fix flanky test * enable example tests * tidy docker compose * add debug output * stop shutingdown masters * don't test sentinel for re * skip unsuported addscores * Update README bump go version in CI * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CONTRIBUTING.md add information about new test setup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/actions/run-tests/action.yml | 21 +- .github/workflows/build.yml | 62 ++++-- .github/workflows/codeql-analysis.yml | 68 ++++++ .github/workflows/doctests.yaml | 4 +- .github/workflows/test-redis-enterprise.yml | 4 +- .gitignore | 4 +- CONTRIBUTING.md | 23 +- Makefile | 42 ++-- README.md | 14 ++ acl_commands_test.go | 6 +- bench_test.go | 105 +++++---- cluster_commands.go | 7 + commands_test.go | 72 ++++--- docker-compose.yml | 115 ++++------ dockers/.gitignore | 7 +- dockers/sentinel.conf | 4 +- doctests/bf_tutorial_test.go | 2 + doctests/bitfield_tutorial_test.go | 2 + doctests/bitmap_tutorial_test.go | 4 + doctests/cmds_generic_test.go | 6 + doctests/cmds_hash_test.go | 8 + doctests/cmds_servermgmt_test.go | 2 + doctests/cmds_sorted_set_test.go | 8 + doctests/cmds_string_test.go | 2 + doctests/cms_tutorial_test.go | 2 + doctests/cuckoo_tutorial_test.go | 2 + doctests/geo_index_test.go | 3 + doctests/geo_tutorial_test.go | 4 + doctests/hash_tutorial_test.go | 8 + doctests/hll_tutorial_test.go | 2 + doctests/home_json_example_test.go | 2 + doctests/json_tutorial_test.go | 8 + doctests/list_tutorial_test.go | 22 ++ doctests/pipe_trans_example_test.go | 2 + doctests/query_agg_test.go | 2 + doctests/query_em_test.go | 15 +- doctests/query_ft_test.go | 2 + doctests/query_geo_test.go | 2 + doctests/set_get_test.go | 2 + doctests/sets_example_test.go | 27 ++- doctests/ss_tutorial_test.go | 16 ++ doctests/stream_tutorial_test.go | 18 ++ doctests/string_example_test.go | 8 + doctests/tdigest_tutorial_test.go | 10 + doctests/topk_tutorial_test.go | 2 + example/hset-struct/go.mod | 2 +- gears_commands_test.go | 7 + internal/pool/pool_test.go | 4 +- iterator_test.go | 2 + main_test.go | 226 +++++--------------- osscluster_test.go | 47 ++-- pubsub_test.go | 7 + race_test.go | 14 +- redis_test.go | 6 +- ring_test.go | 28 --- search_commands.go | 1 + search_test.go | 32 +-- sentinel_test.go | 74 +++---- timeseries_commands_test.go | 4 + universal_test.go | 3 +- 60 files changed, 659 insertions(+), 549 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 95709b5df8..a1b96d88b6 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -21,12 +21,7 @@ runs: CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}" run: | set -e - redis_major_version=$(echo "$REDIS_VERSION" | grep -oP '^\d+') - if (( redis_major_version < 8 )); then - echo "Using redis-stack for module tests" - else - echo "Using redis CE for module tests" - fi + redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+') # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( @@ -36,27 +31,23 @@ runs: ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then - echo "REDIS_MAJOR_VERSION=${redis_major_version}" >> $GITHUB_ENV + echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV echo "REDIS_IMAGE=redis:${{ inputs.redis-version }}" >> $GITHUB_ENV echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV else echo "Version not found in the mapping." exit 1 fi - sleep 10 # time to settle + sleep 10 # wait for redis to start shell: bash - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }} - run: docker compose --profile all up -d + run: | + make docker.start shell: bash - name: Run tests env: RCE_DOCKER: "true" RE_CLUSTER: "false" run: | - go test \ - --ginkgo.skip-file="ring_test.go" \ - --ginkgo.skip-file="sentinel_test.go" \ - --ginkgo.skip-file="pubsub_test.go" \ - --ginkgo.skip-file="gears_commands_test.go" \ - --ginkgo.label-filter="!NonRedisEnterprise" + make test.ci shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afec49d8a5..592e487643 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,13 +10,19 @@ permissions: contents: read jobs: - build: - name: build + + benchmark: + name: benchmark runs-on: ubuntu-latest strategy: fail-fast: false matrix: - go-version: [1.21.x, 1.22.x, 1.23.x] + redis-version: + - "8.0-M03" # 8.0 milestone 4 + - "7.4.2" # should use redis stack 7.4 + go-version: + - "1.23.x" + - "1.24.x" steps: - name: Set up ${{ matrix.go-version }} @@ -27,15 +33,38 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Test - run: make test + - name: Setup Test environment + env: + REDIS_VERSION: ${{ matrix.redis-version }} + CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ matrix.redis-version }}" + run: | + set -e + redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+') + + # Mapping of redis version to redis testing containers + declare -A redis_version_mapping=( + ["8.0-M03"]="8.0-M04-pre" + ["7.4.2"]="rs-7.4.0-v2" + ) + if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then + echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV + echo "REDIS_IMAGE=redis:${{ matrix.redis-version }}" >> $GITHUB_ENV + echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV + else + echo "Version not found in the mapping." + exit 1 + fi + shell: bash + - name: Set up Docker Compose environment with redis ${{ matrix.redis-version }} + run: make docker.start + shell: bash + - name: Benchmark Tests + env: + RCE_DOCKER: "true" + RE_CLUSTER: "false" + run: make bench + shell: bash - - name: Upload to Codecov - uses: codecov/codecov-action@v5 - with: - files: coverage.txt - token: ${{ secrets.CODECOV_TOKEN }} - test-redis-ce: name: test-redis-ce runs-on: ubuntu-latest @@ -47,11 +76,10 @@ jobs: - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: - - "1.22.x" - "1.23.x" + - "1.24.x" steps: - - name: Checkout code uses: actions/checkout@v4 @@ -60,4 +88,10 @@ jobs: with: go-version: ${{matrix.go-version}} redis-version: ${{ matrix.redis-version }} - + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.txt + token: ${{ secrets.CODECOV_TOKEN }} + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..c4b558f37a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index b04f3140b9..56f882a0e7 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.21", "1.22", "1.23" ] + go-version: ["1.24"] steps: - name: Set up ${{ matrix.go-version }} @@ -38,4 +38,4 @@ jobs: - name: Test doc examples working-directory: ./doctests - run: go test + run: go test -v diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 10c27198a9..6b533aaa1a 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.23.x] + go-version: [1.24.x] re-build: ["7.4.2-54"] steps: @@ -47,7 +47,7 @@ jobs: - name: Test env: RE_CLUSTER: true - REDIS_MAJOR_VERSION: 7 + REDIS_VERSION: "7.4" run: | go test \ --ginkgo.skip-file="ring_test.go" \ diff --git a/.gitignore b/.gitignore index a02e552172..e9c8f52641 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ testdata/* *.tar.gz *.dic redis8tests.sh -.vscode +coverage.txt +**/coverage.txt +.vscode \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90030b89f6..bcaee7c731 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,20 +32,33 @@ Here's how to get started with your code contribution: 1. Create your own fork of go-redis 2. Do the changes in your fork -3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running: - ```docker run -p 6379:6379 -it redis/redis-stack-server:edge``` -4. While developing, make sure the tests pass by running `make tests` +3. If you need a development environment, run `make docker.start`. + +> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about +> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity +> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`. +> By default the docker image that will be pulled and started is `redislabs/client-libs-test:rs-7.4.0-v2`. +> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box. + +4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient). +> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers. 5. If you like the change and think the project could use it, send a pull request To see what else is part of the automation, run `invoke -l` + ## Testing -Call `make test` to run all tests, including linters. +### Setting up Docker +To run the tests, you need to have Docker installed and running. If you are using a host OS that does not support +docker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks. + +### Running tests +Call `make test` to run all tests. Continuous Integration uses these same wrappers to run all of these -tests against multiple versions of python. Feel free to test your +tests against multiple versions of redis. Feel free to test your changes against all the go versions supported, as declared by the [build.yml](./.github/workflows/build.yml) file. diff --git a/Makefile b/Makefile index 360505ba5a..d94676ad49 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,35 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) -export REDIS_MAJOR_VERSION := 7 -test: testdeps - docker start go-redis-redis-stack || docker run -d --name go-redis-redis-stack -p 6379:6379 -e REDIS_ARGS="--enable-debug-command yes --enable-module-command yes" redis/redis-stack-server:latest - $(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2)) +docker.start: + docker compose --profile all up -d --quiet-pull + +docker.stop: + docker compose --profile all down + +test: + $(MAKE) docker.start + $(MAKE) test.ci + $(MAKE) docker.stop + +test.ci: set -e; for dir in $(GO_MOD_DIRS); do \ - if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \ - echo "Skipping go test in $${dir} due to Go version 1.19 and dir contains ./example"; \ - continue; \ - fi; \ echo "go test in $${dir}"; \ (cd "$${dir}" && \ go mod tidy -compat=1.18 && \ - go test && \ - go test ./... -short -race && \ - go test ./... -run=NONE -bench=. -benchmem && \ - env GOOS=linux GOARCH=386 go test && \ - go test -coverprofile=coverage.txt -covermode=atomic ./... && \ - go vet); \ + go vet && \ + go test -coverprofile=coverage.txt -covermode=atomic ./... -race); \ done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet - docker stop go-redis-redis-stack -testdeps: testdata/redis/src/redis-server - -bench: testdeps +bench: go test ./... -test.run=NONE -test.bench=. -test.benchmem -.PHONY: all test testdeps bench fmt +.PHONY: all test bench fmt build: go build . -testdata/redis: - mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.4.2.tar.gz | tar xvz --strip-components=1 -C $@ - -testdata/redis/src/redis-server: testdata/redis - cd $< && make all - fmt: gofumpt -w ./ goimports -w -local github.com/redis/go-redis ./ diff --git a/README.md b/README.md index e71367659d..3ab23ba699 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,20 @@ > See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which > demonstrates how you can use Uptrace to monitor go-redis. +## Supported versions + +In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support: +- [Redis 7.2](https://raw.githubusercontent.com/redis/redis/7.2/00-RELEASENOTES) - using Redis Stack 7.2 for modules support +- [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support +- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included + +Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three +versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0), +[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with +Redis Stack 7.2 and some commands are changed with Redis CE 8.0. +Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version +in the `go.mod` to `go 1.24` in one of the next releases. + ## How do I Redis? [Learn for free at Redis University](https://university.redis.com/) diff --git a/acl_commands_test.go b/acl_commands_test.go index 8464558318..a96621dbce 100644 --- a/acl_commands_test.go +++ b/acl_commands_test.go @@ -242,7 +242,7 @@ var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() { }) It("set permissions for module commands", func() { - SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + SkipBeforeRedisVersion(8, "permissions for modules are supported for Redis Version >=8") Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) @@ -322,7 +322,7 @@ var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() { }) It("set permissions for module categories", func() { - SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + SkipBeforeRedisVersion(8, "permissions for modules are supported for Redis Version >=8") Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) @@ -419,7 +419,7 @@ var _ = Describe("ACL Categories", func() { }) It("lists acl categories and subcategories with Modules", func() { - SkipBeforeRedisMajor(8, "modules are included in acl for redis version >= 8") + SkipBeforeRedisVersion(8, "modules are included in acl for redis version >= 8") aclTestCase := map[string]string{ "search": "FT.CREATE", "bloom": "bf.add", diff --git a/bench_test.go b/bench_test.go index bb84c4156d..263216c17b 100644 --- a/bench_test.go +++ b/bench_test.go @@ -277,37 +277,41 @@ func BenchmarkXRead(b *testing.B) { func newClusterScenario() *clusterScenario { return &clusterScenario{ - ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, - nodeIDs: make([]string, 6), - processes: make(map[string]*redisProcess, 6), - clients: make(map[string]*redis.Client, 6), + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, + nodeIDs: make([]string, 6), + clients: make(map[string]*redis.Client, 6), } } +var clusterBench *clusterScenario + func BenchmarkClusterPing(b *testing.B) { if testing.Short() { b.Skip("skipping in short mode") } ctx := context.Background() - cluster := newClusterScenario() - if err := startCluster(ctx, cluster); err != nil { - b.Fatal(err) + if clusterBench == nil { + clusterBench = newClusterScenario() + if err := configureClusterTopology(ctx, clusterBench); err != nil { + b.Fatal(err) + } } - defer cluster.Close() - client := cluster.newClusterClient(ctx, redisClusterOptions()) + client := clusterBench.newClusterClient(ctx, redisClusterOptions()) defer client.Close() - b.ResetTimer() + b.Run("cluster ping", func(b *testing.B) { + b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - err := client.Ping(ctx).Err() - if err != nil { - b.Fatal(err) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Ping(ctx).Err() + if err != nil { + b.Fatal(err) + } } - } + }) }) } @@ -317,23 +321,26 @@ func BenchmarkClusterDoInt(b *testing.B) { } ctx := context.Background() - cluster := newClusterScenario() - if err := startCluster(ctx, cluster); err != nil { - b.Fatal(err) + if clusterBench == nil { + clusterBench = newClusterScenario() + if err := configureClusterTopology(ctx, clusterBench); err != nil { + b.Fatal(err) + } } - defer cluster.Close() - client := cluster.newClusterClient(ctx, redisClusterOptions()) + client := clusterBench.newClusterClient(ctx, redisClusterOptions()) defer client.Close() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - err := client.Do(ctx, "SET", 10, 10).Err() - if err != nil { - b.Fatal(err) + b.Run("cluster do set int", func(b *testing.B) { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Do(ctx, "SET", 10, 10).Err() + if err != nil { + b.Fatal(err) + } } - } + }) }) } @@ -343,26 +350,29 @@ func BenchmarkClusterSetString(b *testing.B) { } ctx := context.Background() - cluster := newClusterScenario() - if err := startCluster(ctx, cluster); err != nil { - b.Fatal(err) + if clusterBench == nil { + clusterBench = newClusterScenario() + if err := configureClusterTopology(ctx, clusterBench); err != nil { + b.Fatal(err) + } } - defer cluster.Close() - client := cluster.newClusterClient(ctx, redisClusterOptions()) + client := clusterBench.newClusterClient(ctx, redisClusterOptions()) defer client.Close() value := string(bytes.Repeat([]byte{'1'}, 10000)) - b.ResetTimer() + b.Run("cluster set string", func(b *testing.B) { + b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - err := client.Set(ctx, "key", value, 0).Err() - if err != nil { - b.Fatal(err) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + err := client.Set(ctx, "key", value, 0).Err() + if err != nil { + b.Fatal(err) + } } - } + }) }) } @@ -372,21 +382,6 @@ func BenchmarkExecRingSetAddrsCmd(b *testing.B) { ringShard2Name = "ringShardTwo" ) - for _, port := range []string{ringShard1Port, ringShard2Port} { - if _, err := startRedis(port); err != nil { - b.Fatal(err) - } - } - - b.Cleanup(func() { - for _, p := range processes { - if err := p.Close(); err != nil { - b.Errorf("Failed to stop redis process: %v", err) - } - } - processes = nil - }) - ring := redis.NewRing(&redis.RingOptions{ Addrs: map[string]string{ "ringShardOne": ":" + ringShard1Port, diff --git a/cluster_commands.go b/cluster_commands.go index 0caf0977a7..4857b01eaa 100644 --- a/cluster_commands.go +++ b/cluster_commands.go @@ -4,6 +4,7 @@ import "context" type ClusterCmdable interface { ClusterMyShardID(ctx context.Context) *StringCmd + ClusterMyID(ctx context.Context) *StringCmd ClusterSlots(ctx context.Context) *ClusterSlotsCmd ClusterShards(ctx context.Context) *ClusterShardsCmd ClusterLinks(ctx context.Context) *ClusterLinksCmd @@ -35,6 +36,12 @@ func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd { return cmd } +func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd { + cmd := NewStringCmd(ctx, "cluster", "myid") + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd { cmd := NewClusterSlotsCmd(ctx, "cluster", "slots") _ = c(ctx, cmd) diff --git a/commands_test.go b/commands_test.go index ff48cfce5e..0bbc3688d8 100644 --- a/commands_test.go +++ b/commands_test.go @@ -194,6 +194,7 @@ var _ = Describe("Commands", func() { }) It("should ClientKillByFilter with MAXAGE", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") var s []string started := make(chan bool) done := make(chan bool) @@ -211,18 +212,18 @@ var _ = Describe("Commands", func() { select { case <-done: Fail("BLPOP is not blocked.") - case <-time.After(1 * time.Second): + case <-time.After(1100 * time.Millisecond): // ok } killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(BeNumerically(">=", 2)) + Expect(killed.Val()).To(BeNumerically(">=", 1)) select { case <-done: // ok - case <-time.After(time.Second): + case <-time.After(200 * time.Millisecond): Fail("BLPOP is still blocked.") } }) @@ -345,7 +346,7 @@ var _ = Describe("Commands", func() { }) It("should ConfigGet Modules", func() { - SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8") expected := map[string]string{ "search-*": "search-timeout", "ts-*": "ts-retention-policy", @@ -380,7 +381,7 @@ var _ = Describe("Commands", func() { }) It("should ConfigGet with Modules", Label("NonRedisEnterprise"), func() { - SkipBeforeRedisMajor(8, "config get won't return modules configs before redis 8") + SkipBeforeRedisVersion(8, "config get won't return modules configs before redis 8") configGet := client.ConfigGet(ctx, "*") Expect(configGet.Err()).NotTo(HaveOccurred()) Expect(configGet.Val()).To(HaveKey("maxmemory")) @@ -391,7 +392,7 @@ var _ = Describe("Commands", func() { }) It("should ConfigSet FT DIALECT", func() { - SkipBeforeRedisMajor(8, "config doesn't include modules before Redis 8") + SkipBeforeRedisVersion(8, "config doesn't include modules before Redis 8") defaultState, err := client.ConfigGet(ctx, "search-default-dialect").Result() Expect(err).NotTo(HaveOccurred()) @@ -437,13 +438,13 @@ var _ = Describe("Commands", func() { }) It("should ConfigSet fail for ReadOnly", func() { - SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8") _, err := client.ConfigSet(ctx, "search-max-doctablesize", "100000").Result() Expect(err).To(HaveOccurred()) }) It("should ConfigSet Modules", func() { - SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8") defaults := map[string]string{} expected := map[string]string{ "search-timeout": "100", @@ -484,7 +485,7 @@ var _ = Describe("Commands", func() { }) It("should Fail ConfigSet Modules", func() { - SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + SkipBeforeRedisVersion(8, "Config doesn't include modules before Redis 8") expected := map[string]string{ "search-timeout": "-100", "ts-retention-policy": "-10", @@ -533,7 +534,7 @@ var _ = Describe("Commands", func() { }) It("should Info Modules", Label("redis.info"), func() { - SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + SkipBeforeRedisVersion(8, "modules are included in info for Redis Version >= 8") info := client.Info(ctx) Expect(info.Err()).NotTo(HaveOccurred()) Expect(info.Val()).NotTo(BeNil()) @@ -558,7 +559,7 @@ var _ = Describe("Commands", func() { }) It("should InfoMap Modules", Label("redis.info"), func() { - SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + SkipBeforeRedisVersion(8, "modules are included in info for Redis Version >= 8") info := client.InfoMap(ctx) Expect(info.Err()).NotTo(HaveOccurred()) Expect(info.Val()).NotTo(BeNil()) @@ -701,8 +702,8 @@ var _ = Describe("Commands", func() { }) }) - Describe("debugging", func() { - PIt("should DebugObject", func() { + Describe("debugging", Label("NonRedisEnterprise"), func() { + It("should DebugObject", func() { err := client.DebugObject(ctx, "foo").Err() Expect(err).To(MatchError("ERR no such key")) @@ -1332,6 +1333,7 @@ var _ = Describe("Commands", func() { }) It("should HScan without values", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") for i := 0; i < 1000; i++ { sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") Expect(sadd.Err()).NotTo(HaveOccurred()) @@ -2625,6 +2627,7 @@ var _ = Describe("Commands", func() { }) It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2640,6 +2643,7 @@ var _ = Describe("Commands", func() { }) It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(res).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2655,6 +2659,7 @@ var _ = Describe("Commands", func() { }) It("should HExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2670,6 +2675,7 @@ var _ = Describe("Commands", func() { }) It("should HPExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2685,6 +2691,7 @@ var _ = Describe("Commands", func() { }) It("should HPersist", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2708,6 +2715,7 @@ var _ = Describe("Commands", func() { }) It("should HExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2727,6 +2735,7 @@ var _ = Describe("Commands", func() { }) It("should HPExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2747,6 +2756,7 @@ var _ = Describe("Commands", func() { }) It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -2766,6 +2776,7 @@ var _ = Describe("Commands", func() { }) It("should HPTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") resEmpty, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() Expect(err).To(BeNil()) Expect(resEmpty).To(BeEquivalentTo([]int64{-2, -2, -2})) @@ -6040,6 +6051,7 @@ var _ = Describe("Commands", func() { }) It("should XRead LastEntry", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.XRead(ctx, &redis.XReadArgs{ Streams: []string{"stream"}, Count: 2, // we expect 1 message @@ -6057,6 +6069,7 @@ var _ = Describe("Commands", func() { }) It("should XRead LastEntry from two streams", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.XRead(ctx, &redis.XReadArgs{ Streams: []string{"stream", "stream"}, ID: "+", @@ -6079,6 +6092,7 @@ var _ = Describe("Commands", func() { }) It("should XRead LastEntry blocks", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") start := time.Now() go func() { defer GinkgoRecover() @@ -6614,14 +6628,12 @@ var _ = Describe("Commands", func() { res, err := client.ZRangeWithScores(ctx, "result", 0, -1).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(ContainElement(redis.Z{ - Score: 190.44242984775784, - Member: "Palermo", - })) - Expect(res).To(ContainElement(redis.Z{ - Score: 56.4412578701582, - Member: "Catania", - })) + Expect(len(res)).To(Equal(2)) + var palermo, catania redis.Z + Expect(res).To(ContainElement(HaveField("Member", "Palermo"), &palermo)) + Expect(res).To(ContainElement(HaveField("Member", "Catania"), &catania)) + Expect(palermo.Score).To(BeNumerically("~", 190, 1)) + Expect(catania.Score).To(BeNumerically("~", 56, 1)) }) It("should search geo radius with options", func() { @@ -6933,16 +6945,13 @@ var _ = Describe("Commands", func() { v, err := client.ZRangeWithScores(ctx, "key2", 0, -1).Result() Expect(err).NotTo(HaveOccurred()) - Expect(v).To(Equal([]redis.Z{ - { - Score: 56.441257870158204, - Member: "Catania", - }, - { - Score: 190.44242984775784, - Member: "Palermo", - }, - })) + + Expect(len(v)).To(Equal(2)) + var palermo, catania redis.Z + Expect(v).To(ContainElement(HaveField("Member", "Palermo"), &palermo)) + Expect(v).To(ContainElement(HaveField("Member", "Catania"), &catania)) + Expect(palermo.Score).To(BeNumerically("~", 190, 1)) + Expect(catania.Score).To(BeNumerically("~", 56, 1)) }) }) @@ -7332,6 +7341,7 @@ var _ = Describe("Commands", func() { }) It("Shows function stats", func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") defer client.FunctionKill(ctx) // We can not run blocking commands in Redis functions, so we're using an infinite loop, diff --git a/docker-compose.yml b/docker-compose.yml index fecd14feff..5bf69f19d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: redis: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -21,9 +21,9 @@ services: - all-stack - all - cluster: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} - container_name: redis-cluster + osscluster: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + container_name: redis-osscluster environment: - NODES=6 - PORT=16600 @@ -31,110 +31,71 @@ services: ports: - "16600-16605:16600-16605" volumes: - - "./dockers/cluster:/redis/work" + - "./dockers/osscluster:/redis/work" profiles: - cluster - all-stack - all - sentinel: - image: ${REDIS_IMAGE:-redis:7.4.1} - container_name: redis-sentinel - depends_on: - - redis - entrypoint: "redis-sentinel /redis.conf --port 26379" - ports: - - 26379:26379 - volumes: - - "./dockers/sentinel.conf:/redis.conf" - profiles: - - sentinel - - all-stack - - all - - sentinel2: - image: ${REDIS_IMAGE:-redis:7.4.1} - container_name: redis-sentinel2 - depends_on: - - redis - entrypoint: "redis-sentinel /redis.conf --port 26380" - ports: - - 26380:26380 - volumes: - - "./dockers/sentinel.conf:/redis.conf" - profiles: - - sentinel - - all-stack - - all - - sentinel3: - image: ${REDIS_IMAGE:-redis:7.4.1} - container_name: redis-sentinel3 - depends_on: - - redis - entrypoint: "redis-sentinel /redis.conf --port 26381" - ports: - - 26381:26381 - volumes: - - "./dockers/sentinel.conf:/redis.conf" - profiles: - - sentinel - - all-stack - - all - - redisRing1: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} - container_name: redis-ring-1 + sentinel-cluster: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + container_name: redis-sentinel-cluster + network_mode: "host" environment: + - NODES=3 - TLS_ENABLED=yes - REDIS_CLUSTER=no - - PORT=6390 + - PORT=9121 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} - ports: - - 6390:6390 + #ports: + # - "9121-9123:9121-9123" volumes: - - "./dockers/ring1:/redis/work" + - "./dockers/sentinel-cluster:/redis/work" profiles: - - ring - - cluster - sentinel - all-stack - all - redisRing2: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} - container_name: redis-ring-2 + sentinel: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + container_name: redis-sentinel + depends_on: + - sentinel-cluster environment: - - TLS_ENABLED=yes + - NODES=3 - REDIS_CLUSTER=no - - PORT=6391 - command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} - ports: - - 6391:6391 + - PORT=26379 + command: ${REDIS_EXTRA_ARGS:---sentinel} + network_mode: "host" + #ports: + # - 26379:26379 + # - 26380:26380 + # - 26381:26381 volumes: - - "./dockers/ring2:/redis/work" + - "./dockers/sentinel.conf:/redis/config-default/redis.conf" + - "./dockers/sentinel:/redis/work" profiles: - - ring - - cluster - sentinel - all-stack - all - redisRing3: - image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} - container_name: redis-ring-3 + ring-cluster: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + container_name: redis-ring-cluster environment: + - NODES=3 - TLS_ENABLED=yes - REDIS_CLUSTER=no - - PORT=6392 + - PORT=6390 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} ports: + - 6390:6390 + - 6391:6391 - 6392:6392 volumes: - - "./dockers/ring3:/redis/work" + - "./dockers/ring:/redis/work" profiles: - ring - cluster - - sentinel - all-stack - - all \ No newline at end of file + - all diff --git a/dockers/.gitignore b/dockers/.gitignore index 355164c126..e24ffee43f 100644 --- a/dockers/.gitignore +++ b/dockers/.gitignore @@ -1 +1,6 @@ -*/ +osscluster/ +ring/ +standalone/ +sentinel-cluster/ +sentinel/ + diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf index 7d85e430a8..3308a1faea 100644 --- a/dockers/sentinel.conf +++ b/dockers/sentinel.conf @@ -1,5 +1,5 @@ sentinel resolve-hostnames yes -sentinel monitor go-redis-test redis 6379 2 +sentinel monitor go-redis-test 127.0.0.1 9121 2 sentinel down-after-milliseconds go-redis-test 5000 sentinel failover-timeout go-redis-test 60000 -sentinel parallel-syncs go-redis-test 1 +sentinel parallel-syncs go-redis-test 1 \ No newline at end of file diff --git a/doctests/bf_tutorial_test.go b/doctests/bf_tutorial_test.go index 67545f1d5e..bd7b310f27 100644 --- a/doctests/bf_tutorial_test.go +++ b/doctests/bf_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_bloom() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:models") // REMOVE_END diff --git a/doctests/bitfield_tutorial_test.go b/doctests/bitfield_tutorial_test.go index 04fcb35f24..9693a6ec7f 100644 --- a/doctests/bitfield_tutorial_test.go +++ b/doctests/bitfield_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_bf() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1:stats") // REMOVE_END diff --git a/doctests/bitmap_tutorial_test.go b/doctests/bitmap_tutorial_test.go index dbfc247ac9..c622f4b887 100644 --- a/doctests/bitmap_tutorial_test.go +++ b/doctests/bitmap_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_ping() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "pings:2024-01-01-00:00") // REMOVE_END @@ -66,6 +68,8 @@ func ExampleClient_bitcount() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() if err != nil { diff --git a/doctests/cmds_generic_test.go b/doctests/cmds_generic_test.go index ab8ebdd53f..18581e9311 100644 --- a/doctests/cmds_generic_test.go +++ b/doctests/cmds_generic_test.go @@ -23,6 +23,8 @@ func ExampleClient_del_cmd() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "key1", "key2", "key3") // REMOVE_END @@ -68,6 +70,8 @@ func ExampleClient_expire_cmd() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "mykey") // REMOVE_END @@ -167,6 +171,8 @@ func ExampleClient_ttl_cmd() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "mykey") // REMOVE_END diff --git a/doctests/cmds_hash_test.go b/doctests/cmds_hash_test.go index 52ade74e98..8a4fdec42c 100644 --- a/doctests/cmds_hash_test.go +++ b/doctests/cmds_hash_test.go @@ -22,6 +22,8 @@ func ExampleClient_hset() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myhash") // REMOVE_END @@ -112,6 +114,8 @@ func ExampleClient_hget() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myhash") // REMOVE_END @@ -157,6 +161,8 @@ func ExampleClient_hgetall() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myhash") // REMOVE_END @@ -209,6 +215,8 @@ func ExampleClient_hvals() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myhash") // REMOVE_END diff --git a/doctests/cmds_servermgmt_test.go b/doctests/cmds_servermgmt_test.go index 8114abc1b9..0cd98f7dd4 100644 --- a/doctests/cmds_servermgmt_test.go +++ b/doctests/cmds_servermgmt_test.go @@ -22,6 +22,8 @@ func ExampleClient_cmd_flushall() { // STEP_START flushall // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Set(ctx, "testkey1", "1", 0) rdb.Set(ctx, "testkey2", "2", 0) rdb.Set(ctx, "testkey3", "3", 0) diff --git a/doctests/cmds_sorted_set_test.go b/doctests/cmds_sorted_set_test.go index 8704fc20d5..d781a2bb90 100644 --- a/doctests/cmds_sorted_set_test.go +++ b/doctests/cmds_sorted_set_test.go @@ -21,6 +21,8 @@ func ExampleClient_zadd_cmd() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myzset") // REMOVE_END @@ -82,6 +84,8 @@ func ExampleClient_zrange1() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myzset") // REMOVE_END @@ -140,6 +144,8 @@ func ExampleClient_zrange2() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myzset") // REMOVE_END @@ -180,6 +186,8 @@ func ExampleClient_zrange3() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "myzset") // REMOVE_END diff --git a/doctests/cmds_string_test.go b/doctests/cmds_string_test.go index fb7801a673..3808be9d36 100644 --- a/doctests/cmds_string_test.go +++ b/doctests/cmds_string_test.go @@ -21,6 +21,8 @@ func ExampleClient_cmd_incr() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "mykey") // REMOVE_END diff --git a/doctests/cms_tutorial_test.go b/doctests/cms_tutorial_test.go index ade1fa93da..e84314a01d 100644 --- a/doctests/cms_tutorial_test.go +++ b/doctests/cms_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_cms() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:profit") // REMOVE_END diff --git a/doctests/cuckoo_tutorial_test.go b/doctests/cuckoo_tutorial_test.go index 08a503b10e..4159d2ba4e 100644 --- a/doctests/cuckoo_tutorial_test.go +++ b/doctests/cuckoo_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_cuckoo() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:models") // REMOVE_END diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go index 9c38ba9d3e..c497b7224e 100644 --- a/doctests/geo_index_test.go +++ b/doctests/geo_index_test.go @@ -20,7 +20,10 @@ func ExampleClient_geoindex() { DB: 0, // use default DB Protocol: 2, }) + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.FTDropIndex(ctx, "productidx") rdb.FTDropIndex(ctx, "geomidx") rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") diff --git a/doctests/geo_tutorial_test.go b/doctests/geo_tutorial_test.go index 051db623b4..a3f6f12163 100644 --- a/doctests/geo_tutorial_test.go +++ b/doctests/geo_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_geoadd() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:rentable") // REMOVE_END @@ -81,6 +83,8 @@ func ExampleClient_geosearch() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:rentable") _, err := rdb.GeoAdd(ctx, "bikes:rentable", diff --git a/doctests/hash_tutorial_test.go b/doctests/hash_tutorial_test.go index 8b0b1ce9a7..ebaa23fac8 100644 --- a/doctests/hash_tutorial_test.go +++ b/doctests/hash_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_set_get_all() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1") // REMOVE_END @@ -102,6 +104,8 @@ func ExampleClient_hmget() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1") // REMOVE_END @@ -160,6 +164,8 @@ func ExampleClient_hincrby() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1") // REMOVE_END @@ -209,6 +215,8 @@ func ExampleClient_incrby_get_mget() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1:stats") // REMOVE_END diff --git a/doctests/hll_tutorial_test.go b/doctests/hll_tutorial_test.go index 57e78d1081..f8cd16dc70 100644 --- a/doctests/hll_tutorial_test.go +++ b/doctests/hll_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_pfadd() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes") // REMOVE_END diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index b9e46a638a..ec2843ad35 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -26,6 +26,8 @@ func ExampleClient_search_json() { }) // STEP_END // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "user:1", "user:2", "user:3") rdb.FTDropIndex(ctx, "idx:users") // REMOVE_END diff --git a/doctests/json_tutorial_test.go b/doctests/json_tutorial_test.go index 4e9787330a..5c2992573b 100644 --- a/doctests/json_tutorial_test.go +++ b/doctests/json_tutorial_test.go @@ -20,6 +20,8 @@ func ExampleClient_setget() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike") // REMOVE_END @@ -67,6 +69,8 @@ func ExampleClient_str() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike") // REMOVE_END @@ -120,6 +124,8 @@ func ExampleClient_num() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "crashes") // REMOVE_END @@ -174,6 +180,8 @@ func ExampleClient_arr() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "newbike") // REMOVE_END diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go index bec1e16435..1df413d4b5 100644 --- a/doctests/list_tutorial_test.go +++ b/doctests/list_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_queue() { }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -75,6 +77,8 @@ func ExampleClient_stack() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -129,6 +133,8 @@ func ExampleClient_llen() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -156,6 +162,8 @@ func ExampleClient_lmove_lrange() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") rdb.Del(ctx, "bikes:finished") // REMOVE_END @@ -220,6 +228,8 @@ func ExampleClient_lpush_rpush() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -274,6 +284,8 @@ func ExampleClient_variadic() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -319,6 +331,8 @@ func ExampleClient_lpop_rpop() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -384,6 +398,8 @@ func ExampleClient_ltrim() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -429,6 +445,8 @@ func ExampleClient_ltrim_end_of_list() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -474,6 +492,8 @@ func ExampleClient_brpop() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:repairs") // REMOVE_END @@ -529,6 +549,8 @@ func ExampleClient_rule1() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "new_bikes") // REMOVE_END diff --git a/doctests/pipe_trans_example_test.go b/doctests/pipe_trans_example_test.go index ea1dd5b482..4ce3840ff9 100644 --- a/doctests/pipe_trans_example_test.go +++ b/doctests/pipe_trans_example_test.go @@ -20,6 +20,8 @@ func ExampleClient_transactions() { DB: 0, // use default DB }) // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) for i := 0; i < 5; i++ { rdb.Del(ctx, fmt.Sprintf("seat:%d", i)) } diff --git a/doctests/query_agg_test.go b/doctests/query_agg_test.go index a710087e48..baa5dfbae1 100644 --- a/doctests/query_agg_test.go +++ b/doctests/query_agg_test.go @@ -21,6 +21,8 @@ func ExampleClient_query_agg() { }) // HIDE_END // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.FTDropIndex(ctx, "idx:bicycle") rdb.FTDropIndex(ctx, "idx:email") // REMOVE_END diff --git a/doctests/query_em_test.go b/doctests/query_em_test.go index d4267df4f9..feb9184133 100644 --- a/doctests/query_em_test.go +++ b/doctests/query_em_test.go @@ -5,6 +5,8 @@ package example_commands_test import ( "context" "fmt" + "slices" + "strings" "github.com/redis/go-redis/v9" ) @@ -21,6 +23,8 @@ func ExampleClient_query_em() { // HIDE_END // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.FTDropIndex(ctx, "idx:bicycle") rdb.FTDropIndex(ctx, "idx:email") // REMOVE_END @@ -274,11 +278,16 @@ func ExampleClient_query_em() { fmt.Println(res3.Total) // >>> 5 - for _, doc := range res3.Docs { + docs := res3.Docs + slices.SortFunc(docs, func(a, b redis.Document) int { + return strings.Compare(a.ID, b.ID) + }) + + for _, doc := range docs { fmt.Println(doc.ID) } - // >>> bicycle:5 // >>> bicycle:0 + // >>> bicycle:5 // >>> bicycle:6 // >>> bicycle:7 // >>> bicycle:8 @@ -350,8 +359,8 @@ func ExampleClient_query_em() { // 1 // bicycle:0 // 5 - // bicycle:5 // bicycle:0 + // bicycle:5 // bicycle:6 // bicycle:7 // bicycle:8 diff --git a/doctests/query_ft_test.go b/doctests/query_ft_test.go index 095230f739..0b3710a979 100644 --- a/doctests/query_ft_test.go +++ b/doctests/query_ft_test.go @@ -21,6 +21,8 @@ func ExampleClient_query_ft() { }) // HIDE_END // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.FTDropIndex(ctx, "idx:bicycle") rdb.FTDropIndex(ctx, "idx:email") // REMOVE_END diff --git a/doctests/query_geo_test.go b/doctests/query_geo_test.go index 7e880aead1..b0287e0fba 100644 --- a/doctests/query_geo_test.go +++ b/doctests/query_geo_test.go @@ -21,6 +21,8 @@ func ExampleClient_query_geo() { }) // HIDE_END // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.FTDropIndex(ctx, "idx:bicycle") // REMOVE_END diff --git a/doctests/set_get_test.go b/doctests/set_get_test.go index ab3a936036..63852eff8d 100644 --- a/doctests/set_get_test.go +++ b/doctests/set_get_test.go @@ -21,6 +21,8 @@ func ExampleClient_Set_and_get() { // HIDE_END // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test if errFlush != nil { panic(errFlush) diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go index 2d6504e2b1..15cabf0ad7 100644 --- a/doctests/sets_example_test.go +++ b/doctests/sets_example_test.go @@ -21,6 +21,8 @@ func ExampleClient_sadd() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") rdb.Del(ctx, "bikes:racing:usa") // REMOVE_END @@ -76,6 +78,8 @@ func ExampleClient_sismember() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") rdb.Del(ctx, "bikes:racing:usa") // REMOVE_END @@ -125,6 +129,8 @@ func ExampleClient_sinter() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") rdb.Del(ctx, "bikes:racing:usa") // REMOVE_END @@ -165,6 +171,8 @@ func ExampleClient_scard() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") // REMOVE_END @@ -198,6 +206,8 @@ func ExampleClient_saddsmembers() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") // REMOVE_END @@ -216,7 +226,7 @@ func ExampleClient_saddsmembers() { panic(err) } - // Sort the strings in the slice to make sure the output is lexicographical + // Sort the strings in the slice to make sure the output is lexicographical sort.Strings(res10) fmt.Println(res10) // >>> [bike:1 bike:2 bike:3] @@ -237,6 +247,8 @@ func ExampleClient_smismember() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") // REMOVE_END @@ -279,6 +291,8 @@ func ExampleClient_sdiff() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") rdb.Del(ctx, "bikes:racing:usa") // REMOVE_END @@ -298,8 +312,7 @@ func ExampleClient_sdiff() { panic(err) } - - // Sort the strings in the slice to make sure the output is lexicographical + // Sort the strings in the slice to make sure the output is lexicographical sort.Strings(res13) fmt.Println(res13) // >>> [bike:2 bike:3] @@ -319,6 +332,8 @@ func ExampleClient_multisets() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") rdb.Del(ctx, "bikes:racing:usa") rdb.Del(ctx, "bikes:racing:italy") @@ -357,7 +372,7 @@ func ExampleClient_multisets() { panic(err) } - // Sort the strings in the slice to make sure the output is lexicographical + // Sort the strings in the slice to make sure the output is lexicographical sort.Strings(res15) fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4] @@ -384,7 +399,7 @@ func ExampleClient_multisets() { panic(err) } - // Sort the strings in the slice to make sure the output is lexicographical + // Sort the strings in the slice to make sure the output is lexicographical sort.Strings(res18) fmt.Println(res18) // >>> [bike:2 bike:3] @@ -408,6 +423,8 @@ func ExampleClient_srem() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:racing:france") // REMOVE_END diff --git a/doctests/ss_tutorial_test.go b/doctests/ss_tutorial_test.go index 2a6924458d..35687e7e98 100644 --- a/doctests/ss_tutorial_test.go +++ b/doctests/ss_tutorial_test.go @@ -20,6 +20,8 @@ func ExampleClient_zadd() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -76,6 +78,8 @@ func ExampleClient_zrange() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -127,6 +131,8 @@ func ExampleClient_zrangewithscores() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -168,6 +174,8 @@ func ExampleClient_zrangebyscore() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -211,6 +219,8 @@ func ExampleClient_zremrangebyscore() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -270,6 +280,8 @@ func ExampleClient_zrank() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -316,6 +328,8 @@ func ExampleClient_zaddlex() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END @@ -377,6 +391,8 @@ func ExampleClient_leaderboard() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_scores") // REMOVE_END diff --git a/doctests/stream_tutorial_test.go b/doctests/stream_tutorial_test.go index 0933247056..e39919ea62 100644 --- a/doctests/stream_tutorial_test.go +++ b/doctests/stream_tutorial_test.go @@ -26,6 +26,8 @@ func ExampleClient_xadd() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:france") // REMOVE_END @@ -105,6 +107,8 @@ func ExampleClient_racefrance1() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:france") // REMOVE_END @@ -227,6 +231,8 @@ func ExampleClient_raceusa() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:usa") // REMOVE_END @@ -310,6 +316,8 @@ func ExampleClient_racefrance2() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:france") // REMOVE_END @@ -478,6 +486,8 @@ func ExampleClient_xgroupcreate() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:france") // REMOVE_END @@ -520,6 +530,8 @@ func ExampleClient_xgroupcreatemkstream() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:italy") // REMOVE_END @@ -549,6 +561,8 @@ func ExampleClient_xgroupread() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:italy") // REMOVE_END @@ -654,6 +668,8 @@ func ExampleClient_raceitaly() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:italy") rdb.XGroupDestroy(ctx, "race:italy", "italy_riders") // REMOVE_END @@ -1011,6 +1027,8 @@ func ExampleClient_xdel() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "race:italy") // REMOVE_END diff --git a/doctests/string_example_test.go b/doctests/string_example_test.go index 20ca855489..025659fe87 100644 --- a/doctests/string_example_test.go +++ b/doctests/string_example_test.go @@ -20,6 +20,8 @@ func ExampleClient_set_get() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1") // REMOVE_END @@ -56,6 +58,8 @@ func ExampleClient_setnx_xx() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Set(ctx, "bike:1", "Deimos", 0) // REMOVE_END @@ -101,6 +105,8 @@ func ExampleClient_mset() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bike:1", "bike:2", "bike:3") // REMOVE_END @@ -137,6 +143,8 @@ func ExampleClient_incr() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "total_crashes") // REMOVE_END diff --git a/doctests/tdigest_tutorial_test.go b/doctests/tdigest_tutorial_test.go index 7589b0ec89..9cda4c373c 100644 --- a/doctests/tdigest_tutorial_test.go +++ b/doctests/tdigest_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_tdigstart() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_ages", "bikes:sales") // REMOVE_END @@ -69,6 +71,8 @@ func ExampleClient_tdigcdf() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_ages", "bikes:sales") // REMOVE_END @@ -126,6 +130,8 @@ func ExampleClient_tdigquant() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_ages") // REMOVE_END @@ -177,6 +183,8 @@ func ExampleClient_tdigmin() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_ages") // REMOVE_END @@ -228,6 +236,8 @@ func ExampleClient_tdigreset() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "racer_ages") // REMOVE_END _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() diff --git a/doctests/topk_tutorial_test.go b/doctests/topk_tutorial_test.go index 2d1fe7fc22..db33620597 100644 --- a/doctests/topk_tutorial_test.go +++ b/doctests/topk_tutorial_test.go @@ -21,6 +21,8 @@ func ExampleClient_topk() { }) // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) rdb.Del(ctx, "bikes:keywords") // REMOVE_END diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index fca1a59720..f14f54df1f 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -10,6 +10,6 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/gears_commands_test.go b/gears_commands_test.go index b1117a4dc2..7d30995853 100644 --- a/gears_commands_test.go +++ b/gears_commands_test.go @@ -34,6 +34,7 @@ func libCodeWithConfig(libName string) string { return fmt.Sprintf(lib, libName) } +// TODO: Drop Gears var _ = Describe("RedisGears commands", Label("gears"), func() { ctx := context.TODO() var client *redis.Client @@ -49,6 +50,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { }) It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) Expect(resultAdd).To(BeEquivalentTo("OK")) @@ -58,6 +60,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { Expect(resultAdd).To(BeEquivalentTo("OK")) }) It("should TFunctionList", Label("gears", "tfunctionlist"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) Expect(resultAdd).To(BeEquivalentTo("OK")) @@ -71,6 +74,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { }) It("should TFCall", Label("gears", "tfcall"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") var resultAdd interface{} resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) @@ -81,6 +85,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { }) It("should TFCallArgs", Label("gears", "tfcallargs"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") var resultAdd interface{} resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) @@ -92,6 +97,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { }) It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") var resultAdd interface{} resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) @@ -102,6 +108,7 @@ var _ = Describe("RedisGears commands", Label("gears"), func() { }) It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() { + SkipAfterRedisVersion(7.4, "gears are not working in later versions") var resultAdd interface{} resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() Expect(err).NotTo(HaveOccurred()) diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 76dec996b5..4ccc489306 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -292,8 +292,8 @@ var _ = Describe("race", func() { BeforeEach(func() { C, N = 10, 1000 if testing.Short() { - C = 4 - N = 100 + C = 2 + N = 50 } }) diff --git a/iterator_test.go b/iterator_test.go index 472ce38a7d..c4f0464766 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -85,6 +85,7 @@ var _ = Describe("ScanIterator", func() { }) It("should hscan across multiple pages", func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") Expect(hashSeed(71)).NotTo(HaveOccurred()) var vals []string @@ -100,6 +101,7 @@ var _ = Describe("ScanIterator", func() { }) It("should hscan without values across multiple pages", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") Expect(hashSeed(71)).NotTo(HaveOccurred()) var vals []string diff --git a/main_test.go b/main_test.go index a326960a0f..556e633e53 100644 --- a/main_test.go +++ b/main_test.go @@ -4,9 +4,8 @@ import ( "fmt" "net" "os" - "os/exec" - "path/filepath" "strconv" + "strings" "sync" "testing" "time" @@ -28,12 +27,12 @@ const ( const ( sentinelName = "go-redis-test" - sentinelMasterPort = "9123" - sentinelSlave1Port = "9124" - sentinelSlave2Port = "9125" - sentinelPort1 = "9126" - sentinelPort2 = "9127" - sentinelPort3 = "9128" + sentinelMasterPort = "9121" + sentinelSlave1Port = "9122" + sentinelSlave2Port = "9123" + sentinelPort1 = "26379" + sentinelPort2 = "26380" + sentinelPort3 = "26381" ) var ( @@ -49,19 +48,15 @@ var ( var ( sentinelAddrs = []string{":" + sentinelPort1, ":" + sentinelPort2, ":" + sentinelPort3} - processes map[string]*redisProcess - - redisMain *redisProcess - ringShard1, ringShard2, ringShard3 *redisProcess - sentinelMaster, sentinelSlave1, sentinelSlave2 *redisProcess - sentinel1, sentinel2, sentinel3 *redisProcess + ringShard1, ringShard2, ringShard3 *redis.Client + sentinelMaster, sentinelSlave1, sentinelSlave2 *redis.Client + sentinel1, sentinel2, sentinel3 *redis.Client ) var cluster = &clusterScenario{ - ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, - nodeIDs: make([]string, 6), - processes: make(map[string]*redisProcess, 6), - clients: make(map[string]*redis.Client, 6), + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, + nodeIDs: make([]string, 6), + clients: make(map[string]*redis.Client, 6), } // Redis Software Cluster @@ -70,30 +65,23 @@ var RECluster = false // Redis Community Edition Docker var RCEDocker = false -// Notes the major version of redis we are executing tests. +// Notes version of redis we are executing tests against. // This can be used before we change the bsm fork of ginkgo for one, -// which have support for label sets, so we can filter tests per redis major version. -var RedisMajorVersion = 7 +// which have support for label sets, so we can filter tests per redis version. +var RedisVersion float64 = 7.2 -func SkipBeforeRedisMajor(version int, msg string) { - if RedisMajorVersion < version { - Skip(fmt.Sprintf("(redis major version < %d) %s", version, msg)) +func SkipBeforeRedisVersion(version float64, msg string) { + if RedisVersion < version { + Skip(fmt.Sprintf("(redis version < %f) %s", version, msg)) } } -func SkipAfterRedisMajor(version int, msg string) { - if RedisMajorVersion > version { - Skip(fmt.Sprintf("(redis major version > %d) %s", version, msg)) +func SkipAfterRedisVersion(version float64, msg string) { + if RedisVersion > version { + Skip(fmt.Sprintf("(redis version > %f) %s", version, msg)) } } -func registerProcess(port string, p *redisProcess) { - if processes == nil { - processes = make(map[string]*redisProcess) - } - processes[port] = p -} - var _ = BeforeSuite(func() { addr := os.Getenv("REDIS_PORT") if addr != "" { @@ -104,35 +92,33 @@ var _ = BeforeSuite(func() { RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) - RedisMajorVersion, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + RedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv("REDIS_VERSION"), "\""), 64) - if RedisMajorVersion == 0 { - RedisMajorVersion = 7 + if RedisVersion == 0 { + RedisVersion = 7.2 } fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_MAJOR_VERSION: %v\n", RedisMajorVersion) + fmt.Printf("REDIS_VERSION: %v\n", RedisVersion) - if RedisMajorVersion < 6 || RedisMajorVersion > 8 { - panic("incorrect or not supported redis major version") + if RedisVersion < 7.0 || RedisVersion > 9 { + panic("incorrect or not supported redis version") } - if !RECluster && !RCEDocker { - - redisMain, err = startRedis(redisPort) - Expect(err).NotTo(HaveOccurred()) - - ringShard1, err = startRedis(ringShard1Port) + redisPort = redisStackPort + redisAddr = redisStackAddr + if !RECluster { + ringShard1, err = connectTo(ringShard1Port) Expect(err).NotTo(HaveOccurred()) - ringShard2, err = startRedis(ringShard2Port) + ringShard2, err = connectTo(ringShard2Port) Expect(err).NotTo(HaveOccurred()) - ringShard3, err = startRedis(ringShard3Port) + ringShard3, err = connectTo(ringShard3Port) Expect(err).NotTo(HaveOccurred()) - sentinelMaster, err = startRedis(sentinelMasterPort) + sentinelMaster, err = connectTo(sentinelMasterPort) Expect(err).NotTo(HaveOccurred()) sentinel1, err = startSentinel(sentinelPort1, sentinelName, sentinelMasterPort) @@ -144,24 +130,20 @@ var _ = BeforeSuite(func() { sentinel3, err = startSentinel(sentinelPort3, sentinelName, sentinelMasterPort) Expect(err).NotTo(HaveOccurred()) - sentinelSlave1, err = startRedis( - sentinelSlave1Port, "--slaveof", "127.0.0.1", sentinelMasterPort) + sentinelSlave1, err = connectTo(sentinelSlave1Port) Expect(err).NotTo(HaveOccurred()) - sentinelSlave2, err = startRedis( - sentinelSlave2Port, "--slaveof", "127.0.0.1", sentinelMasterPort) + err = sentinelSlave1.SlaveOf(ctx, "127.0.0.1", sentinelMasterPort).Err() Expect(err).NotTo(HaveOccurred()) - err = startCluster(ctx, cluster) + sentinelSlave2, err = connectTo(sentinelSlave2Port) Expect(err).NotTo(HaveOccurred()) - } else { - redisPort = redisStackPort - redisAddr = redisStackAddr - if !RECluster { - // populate cluster node information - Expect(configureClusterTopology(ctx, cluster)).NotTo(HaveOccurred()) - } + err = sentinelSlave2.SlaveOf(ctx, "127.0.0.1", sentinelMasterPort).Err() + Expect(err).NotTo(HaveOccurred()) + + // populate cluster node information + Expect(configureClusterTopology(ctx, cluster)).NotTo(HaveOccurred()) } }) @@ -169,12 +151,6 @@ var _ = AfterSuite(func() { if !RECluster { Expect(cluster.Close()).NotTo(HaveOccurred()) } - - // NOOP if there are no processes registered - for _, p := range processes { - Expect(p.Close()).NotTo(HaveOccurred()) - } - processes = nil }) func TestGinkgoSuite(t *testing.T) { @@ -204,7 +180,7 @@ func redisOptions() *redis.Options { } return &redis.Options{ Addr: redisAddr, - DB: 15, + DB: 0, DialTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, @@ -256,7 +232,9 @@ func performAsync(n int, cbs ...func(int)) *sync.WaitGroup { var wg sync.WaitGroup for _, cb := range cbs { wg.Add(n) - for i := 0; i < n; i++ { + // start from 1, so we can skip db 0 where such test is executed with + // select db command + for i := 1; i <= n; i++ { go func(cb func(int), i int) { defer GinkgoRecover() defer wg.Done() @@ -313,15 +291,6 @@ func eventually(fn func() error, timeout time.Duration) error { } } -func execCmd(name string, args ...string) (*os.Process, error) { - cmd := exec.Command(name, args...) - if testing.Verbose() { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - return cmd.Process, cmd.Start() -} - func connectTo(port string) (*redis.Client, error) { client := redis.NewClient(&redis.Options{ Addr: ":" + port, @@ -338,117 +307,22 @@ func connectTo(port string) (*redis.Client, error) { return client, nil } -type redisProcess struct { - *os.Process - *redis.Client -} - -func (p *redisProcess) Close() error { - if err := p.Kill(); err != nil { - return err - } - - err := eventually(func() error { - if err := p.Client.Ping(ctx).Err(); err != nil { - return nil - } - return fmt.Errorf("client %s is not shutdown", p.Options().Addr) - }, 10*time.Second) - if err != nil { - return err - } - - p.Client.Close() - return nil -} - -var ( - redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server")) - redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf")) - redisSentinelConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "sentinel.conf")) -) - -func redisDir(port string) (string, error) { - dir, err := filepath.Abs(filepath.Join("testdata", "instances", port)) - if err != nil { - return "", err - } - if err := os.RemoveAll(dir); err != nil { - return "", err - } - if err := os.MkdirAll(dir, 0o775); err != nil { - return "", err - } - return dir, nil -} - -func startRedis(port string, args ...string) (*redisProcess, error) { - dir, err := redisDir(port) - if err != nil { - return nil, err - } - - if err := exec.Command("cp", "-f", redisServerConf, dir).Run(); err != nil { - return nil, err - } - - baseArgs := []string{filepath.Join(dir, "redis.conf"), "--port", port, "--dir", dir, "--enable-module-command", "yes"} - process, err := execCmd(redisServerBin, append(baseArgs, args...)...) - if err != nil { - return nil, err - } - +func startSentinel(port, masterName, masterPort string) (*redis.Client, error) { client, err := connectTo(port) if err != nil { - process.Kill() return nil, err } - p := &redisProcess{process, client} - registerProcess(port, p) - return p, nil -} - -func startSentinel(port, masterName, masterPort string) (*redisProcess, error) { - dir, err := redisDir(port) - if err != nil { - return nil, err - } - - sentinelConf := filepath.Join(dir, "sentinel.conf") - if err := os.WriteFile(sentinelConf, nil, 0o644); err != nil { - return nil, err - } - - process, err := execCmd(redisServerBin, sentinelConf, "--sentinel", "--port", port, "--dir", dir) - if err != nil { - return nil, err - } - - client, err := connectTo(port) - if err != nil { - process.Kill() - return nil, err - } - - // set down-after-milliseconds=2000 - // link: https://github.com/redis/redis/issues/8607 for _, cmd := range []*redis.StatusCmd{ redis.NewStatusCmd(ctx, "SENTINEL", "MONITOR", masterName, "127.0.0.1", masterPort, "2"), - redis.NewStatusCmd(ctx, "SENTINEL", "SET", masterName, "down-after-milliseconds", "2000"), - redis.NewStatusCmd(ctx, "SENTINEL", "SET", masterName, "failover-timeout", "1000"), - redis.NewStatusCmd(ctx, "SENTINEL", "SET", masterName, "parallel-syncs", "1"), } { client.Process(ctx, cmd) - if err := cmd.Err(); err != nil { - process.Kill() + if err := cmd.Err(); err != nil && !strings.Contains(err.Error(), "ERR Duplicate master name.") { return nil, fmt.Errorf("%s failed: %w", cmd, err) } } - p := &redisProcess{process, client} - registerProcess(port, p) - return p, nil + return client, nil } //------------------------------------------------------------------------------ diff --git a/osscluster_test.go b/osscluster_test.go index 93ee464f3d..aeb34c6bdc 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net" + "slices" "strconv" "strings" "sync" @@ -19,10 +20,9 @@ import ( ) type clusterScenario struct { - ports []string - nodeIDs []string - processes map[string]*redisProcess - clients map[string]*redis.Client + ports []string + nodeIDs []string + clients map[string]*redis.Client } func (s *clusterScenario) slots() []int { @@ -101,20 +101,17 @@ func (s *clusterScenario) Close() error { } } - for _, port := range s.ports { - if process, ok := processes[port]; ok { - if process != nil { - process.Close() - } - - delete(processes, port) - } - } - return nil } func configureClusterTopology(ctx context.Context, scenario *clusterScenario) error { + allowErrs := []string{ + "ERR Slot 0 is already busy", + "ERR Slot 5461 is already busy", + "ERR Slot 10923 is already busy", + "ERR Slot 16384 is already busy", + } + err := collectNodeInformation(ctx, scenario) if err != nil { return err @@ -131,7 +128,7 @@ func configureClusterTopology(ctx context.Context, scenario *clusterScenario) er slots := scenario.slots() for pos, master := range scenario.masters() { err := master.ClusterAddSlotsRange(ctx, slots[pos], slots[pos+1]-1).Err() - if err != nil { + if err != nil && slices.Contains(allowErrs, err.Error()) == false { return err } } @@ -199,7 +196,7 @@ func configureClusterTopology(ctx context.Context, scenario *clusterScenario) er return err } return assertSlotsEqual(res, wanted) - }, 60*time.Second) + }, 90*time.Second) if err != nil { return err } @@ -214,31 +211,17 @@ func collectNodeInformation(ctx context.Context, scenario *clusterScenario) erro Addr: ":" + port, }) - info, err := client.ClusterNodes(ctx).Result() + myID, err := client.ClusterMyID(ctx).Result() if err != nil { return err } scenario.clients[port] = client - scenario.nodeIDs[pos] = info[:40] + scenario.nodeIDs[pos] = myID } return nil } -// startCluster start a cluster -func startCluster(ctx context.Context, scenario *clusterScenario) error { - // Start processes and collect node ids - for _, port := range scenario.ports { - process, err := startRedis(port, "--cluster-enabled", "yes") - if err != nil { - return err - } - scenario.processes[port] = process - } - - return configureClusterTopology(ctx, scenario) -} - func assertSlotsEqual(slots, wanted []redis.ClusterSlot) error { outerLoop: for _, s2 := range wanted { diff --git a/pubsub_test.go b/pubsub_test.go index a761006596..2f3f460452 100644 --- a/pubsub_test.go +++ b/pubsub_test.go @@ -89,6 +89,9 @@ var _ = Describe("PubSub", func() { pubsub := client.Subscribe(ctx, "mychannel", "mychannel2") defer pubsub.Close() + // sleep a bit to make sure redis knows about the subscriptions + time.Sleep(10 * time.Millisecond) + channels, err = client.PubSubChannels(ctx, "mychannel*").Result() Expect(err).NotTo(HaveOccurred()) Expect(channels).To(ConsistOf([]string{"mychannel", "mychannel2"})) @@ -135,6 +138,8 @@ var _ = Describe("PubSub", func() { pubsub := client.Subscribe(ctx, "mychannel", "mychannel2") defer pubsub.Close() + // sleep a bit to make sure redis knows about the subscriptions + time.Sleep(10 * time.Millisecond) channels, err := client.PubSubNumSub(ctx, "mychannel", "mychannel2", "mychannel3").Result() Expect(err).NotTo(HaveOccurred()) Expect(channels).To(Equal(map[string]int64{ @@ -152,6 +157,8 @@ var _ = Describe("PubSub", func() { pubsub := client.PSubscribe(ctx, "*") defer pubsub.Close() + // sleep a bit to make sure redis knows about the subscriptions + time.Sleep(10 * time.Millisecond) num, err = client.PubSubNumPat(ctx).Result() Expect(err).NotTo(HaveOccurred()) Expect(num).To(Equal(int64(1))) diff --git a/race_test.go b/race_test.go index aeb2d1fa30..2c7bd763c6 100644 --- a/race_test.go +++ b/race_test.go @@ -105,7 +105,7 @@ var _ = Describe("races", func() { }) It("should handle big vals in Get", func() { - C, N = 4, 100 + C, N := 4, 100 bigVal := bigVal() @@ -126,7 +126,7 @@ var _ = Describe("races", func() { }) It("should handle big vals in Set", func() { - C, N = 4, 100 + C, N := 4, 100 bigVal := bigVal() perform(C, func(id int) { @@ -138,7 +138,7 @@ var _ = Describe("races", func() { }) It("should select db", Label("NonRedisEnterprise"), func() { - err := client.Set(ctx, "db", 1, 0).Err() + err := client.Set(ctx, "db", 0, 0).Err() Expect(err).NotTo(HaveOccurred()) perform(C, func(id int) { @@ -159,7 +159,7 @@ var _ = Describe("races", func() { n, err := client.Get(ctx, "db").Int64() Expect(err).NotTo(HaveOccurred()) - Expect(n).To(Equal(int64(1))) + Expect(n).To(Equal(int64(0))) }) It("should select DB with read timeout", func() { @@ -214,12 +214,14 @@ var _ = Describe("races", func() { Expect(val).To(Equal(int64(C * N))) }) - PIt("should BLPop", func() { + It("should BLPop", func() { + C := 5 + N := 5 var received uint32 wg := performAsync(C, func(id int) { for { - v, err := client.BLPop(ctx, 5*time.Second, "list").Result() + v, err := client.BLPop(ctx, time.Second, "list").Result() if err != nil { if err == redis.Nil { break diff --git a/redis_test.go b/redis_test.go index b5cf2570f1..f8c91b4a22 100644 --- a/redis_test.go +++ b/redis_test.go @@ -66,11 +66,7 @@ var _ = Describe("Client", func() { }) It("should Stringer", func() { - if RECluster { - Expect(client.String()).To(Equal(fmt.Sprintf("Redis<:%s db:0>", redisPort))) - } else { - Expect(client.String()).To(Equal(fmt.Sprintf("Redis<:%s db:15>", redisPort))) - } + Expect(client.String()).To(Equal(fmt.Sprintf("Redis<:%s db:0>", redisPort))) }) It("supports context", func() { diff --git a/ring_test.go b/ring_test.go index b3017f6167..cfd545c178 100644 --- a/ring_test.go +++ b/ring_test.go @@ -130,34 +130,6 @@ var _ = Describe("Redis Ring", func() { Expect(ringShard2.Info(ctx, "keyspace").Val()).To(ContainSubstring("keys=44")) }) - It("uses single shard when one of the shards is down", func() { - // Stop ringShard2. - Expect(ringShard2.Close()).NotTo(HaveOccurred()) - - Eventually(func() int { - return ring.Len() - }, "30s").Should(Equal(1)) - - setRingKeys() - - // RingShard1 should have all keys. - Expect(ringShard1.Info(ctx, "keyspace").Val()).To(ContainSubstring("keys=100")) - - // Start ringShard2. - var err error - ringShard2, err = startRedis(ringShard2Port) - Expect(err).NotTo(HaveOccurred()) - - Eventually(func() int { - return ring.Len() - }, "30s").Should(Equal(2)) - - setRingKeys() - - // RingShard2 should have its keys. - Expect(ringShard2.Info(ctx, "keyspace").Val()).To(ContainSubstring("keys=44")) - }) - It("supports hash tags", func() { for i := 0; i < 100; i++ { err := ring.Set(ctx, fmt.Sprintf("key%d{tag}", i), "value", 0).Err() diff --git a/search_commands.go b/search_commands.go index 71ee6ab32e..c50ac07fb4 100644 --- a/search_commands.go +++ b/search_commands.go @@ -2079,6 +2079,7 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str return cmd } +// TODO: remove FTProfile // type FTProfileResult struct { // Results []interface{} // Profile ProfileDetails diff --git a/search_test.go b/search_test.go index 1098319e05..d309b1a8be 100644 --- a/search_test.go +++ b/search_test.go @@ -381,7 +381,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - SkipBeforeRedisMajor(8, "default scorer is not BM25") + SkipBeforeRedisVersion(7.9, "default scorer is not BM25") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() @@ -422,9 +422,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 - // this test expect redis major version <=7 + // this test expect redis version < 8.0 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - SkipAfterRedisMajor(7, "default scorer is not TFIDF") + SkipAfterRedisVersion(7.9, "default scorer is not TFIDF") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -464,17 +464,17 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { - val, err := client.FTConfigSet(ctx, "TIMEOUT", "100").Result() + val, err := client.FTConfigSet(ctx, "MINPREFIX", "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) res, err := client.FTConfigGet(ctx, "*").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res["TIMEOUT"]).To(BeEquivalentTo("100")) + Expect(res["MINPREFIX"]).To(BeEquivalentTo("1")) - res, err = client.FTConfigGet(ctx, "TIMEOUT").Result() + res, err = client.FTConfigGet(ctx, "MINPREFIX").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + Expect(res).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"})) }) @@ -667,6 +667,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "no addscores support") title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false} description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result() @@ -1273,6 +1274,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should test dialect 4", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{ Prefix: []interface{}{"resource:"}, }, &redis.FieldSchema{ @@ -1405,6 +1407,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should create search index with FLOAT16 and BFLOAT16 vectors", Label("search", "ftcreate", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") val, err := client.FTCreate(ctx, "index", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "float16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "FLOAT16", Dim: 768, DistanceMetric: "COSINE"}}}, &redis.FieldSchema{FieldName: "bfloat16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: &redis.FTFlatOptions{Type: "BFLOAT16", Dim: 768, DistanceMetric: "COSINE"}}}, @@ -1415,6 +1418,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should test geoshapes query intersects and disjoint", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") _, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{ FieldName: "g", FieldType: redis.SearchFieldTypeGeoShape, @@ -1483,6 +1487,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should search missing fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true}, &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexMissing: true}, @@ -1527,6 +1532,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should search empty fields", Label("search", "ftcreate", "ftsearch", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{Prefix: []interface{}{"property:"}}, &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: true}, &redis.FieldSchema{FieldName: "features", FieldType: redis.SearchFieldTypeTag, IndexEmpty: true}, @@ -1687,18 +1693,18 @@ var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "N Expect(clientResp3.Close()).NotTo(HaveOccurred()) }) - It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { - val, err := clientResp3.FTConfigSet(ctx, "TIMEOUT", "100").Result() + It("should FTConfigSet and FTConfigGet with resp2 and resp3", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + val, err := clientResp3.FTConfigSet(ctx, "MINPREFIX", "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) - res2, err := clientResp2.FTConfigGet(ctx, "TIMEOUT").Result() + res2, err := clientResp2.FTConfigGet(ctx, "MINPREFIX").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res2).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + Expect(res2).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"})) - res3, err := clientResp3.FTConfigGet(ctx, "TIMEOUT").Result() + res3, err := clientResp3.FTConfigGet(ctx, "MINPREFIX").Result() Expect(err).NotTo(HaveOccurred()) - Expect(res3).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + Expect(res3).To(BeEquivalentTo(map[string]interface{}{"MINPREFIX": "1"})) }) It("should FTConfigGet all resp2 and resp3", Label("search", "NonRedisEnterprise"), func() { diff --git a/sentinel_test.go b/sentinel_test.go index 8bc6c57854..b34706f89a 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -6,13 +6,11 @@ import ( . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" - "github.com/redis/go-redis/v9" ) var _ = Describe("Sentinel PROTO 2", func() { var client *redis.Client - BeforeEach(func() { client = redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: sentinelName, @@ -37,7 +35,6 @@ var _ = Describe("Sentinel PROTO 2", func() { var _ = Describe("Sentinel", func() { var client *redis.Client var master *redis.Client - var masterPort string var sentinel *redis.SentinelClient BeforeEach(func() { @@ -61,18 +58,17 @@ var _ = Describe("Sentinel", func() { Addr: net.JoinHostPort(addr[0], addr[1]), MaxRetries: -1, }) - masterPort = addr[1] // Wait until slaves are picked up by sentinel. Eventually(func() string { return sentinel1.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) Eventually(func() string { return sentinel2.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) Eventually(func() string { return sentinel3.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) }) AfterEach(func() { @@ -96,7 +92,7 @@ var _ = Describe("Sentinel", func() { Eventually(func() []string { slavesAddr = redis.GetSlavesAddrByName(ctx, sentinel, sentinelName) return slavesAddr - }, "15s", "100ms").Should(HaveLen(2)) + }, "20s", "50ms").Should(HaveLen(2)) Eventually(func() bool { sync := true for _, addr := range slavesAddr { @@ -108,36 +104,35 @@ var _ = Describe("Sentinel", func() { _ = slave.Close() } return sync - }, "15s", "100ms").Should(BeTrue()) + }, "20s", "50ms").Should(BeTrue()) // Create subscription. pub := client.Subscribe(ctx, "foo") ch := pub.Channel() // Kill master. - err = master.Shutdown(ctx).Err() - Expect(err).NotTo(HaveOccurred()) - Eventually(func() error { - return master.Ping(ctx).Err() - }, "15s", "100ms").Should(HaveOccurred()) + /* + err = master.Shutdown(ctx).Err() + Expect(err).NotTo(HaveOccurred()) + Eventually(func() error { + return master.Ping(ctx).Err() + }, "20s", "50ms").Should(HaveOccurred()) + */ // Check that client picked up new master. Eventually(func() string { return client.Get(ctx, "foo").Val() - }, "15s", "100ms").Should(Equal("master")) + }, "20s", "100ms").Should(Equal("master")) // Check if subscription is renewed. var msg *redis.Message Eventually(func() <-chan *redis.Message { _ = client.Publish(ctx, "foo", "hello").Err() return ch - }, "15s", "100ms").Should(Receive(&msg)) + }, "20s", "100ms").Should(Receive(&msg)) Expect(msg.Channel).To(Equal("foo")) Expect(msg.Payload).To(Equal("hello")) Expect(pub.Close()).NotTo(HaveOccurred()) - - _, err = startRedis(masterPort) - Expect(err).NotTo(HaveOccurred()) }) It("supports DB selection", func() { @@ -197,7 +192,6 @@ var _ = Describe("NewFailoverClusterClient PROTO 2", func() { var _ = Describe("NewFailoverClusterClient", func() { var client *redis.ClusterClient var master *redis.Client - var masterPort string BeforeEach(func() { client = redis.NewFailoverClusterClient(&redis.FailoverOptions{ @@ -221,18 +215,17 @@ var _ = Describe("NewFailoverClusterClient", func() { Addr: net.JoinHostPort(addr[0], addr[1]), MaxRetries: -1, }) - masterPort = addr[1] // Wait until slaves are picked up by sentinel. Eventually(func() string { return sentinel1.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) Eventually(func() string { return sentinel2.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) Eventually(func() string { return sentinel3.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("slaves=2")) + }, "20s", "100ms").Should(ContainSubstring("slaves=2")) }) AfterEach(func() { @@ -241,7 +234,6 @@ var _ = Describe("NewFailoverClusterClient", func() { }) It("should facilitate failover", func() { - Skip("Flaky Test") // Set value. err := client.Set(ctx, "foo", "master", 0).Err() Expect(err).NotTo(HaveOccurred()) @@ -250,7 +242,7 @@ var _ = Describe("NewFailoverClusterClient", func() { // Verify. Eventually(func() string { return client.Get(ctx, "foo").Val() - }, "15s", "1ms").Should(Equal("master")) + }, "20s", "1ms").Should(Equal("master")) } // Create subscription. @@ -258,33 +250,32 @@ var _ = Describe("NewFailoverClusterClient", func() { ch := sub.Channel() // Kill master. - err = master.Shutdown(ctx).Err() - Expect(err).NotTo(HaveOccurred()) - Eventually(func() error { - return master.Ping(ctx).Err() - }, "15s", "100ms").Should(HaveOccurred()) + /* + err = master.Shutdown(ctx).Err() + Expect(err).NotTo(HaveOccurred()) + Eventually(func() error { + return master.Ping(ctx).Err() + }, "20s", "100ms").Should(HaveOccurred()) + */ // Check that client picked up new master. Eventually(func() string { return client.Get(ctx, "foo").Val() - }, "15s", "100ms").Should(Equal("master")) + }, "20s", "100ms").Should(Equal("master")) // Check if subscription is renewed. var msg *redis.Message Eventually(func() <-chan *redis.Message { _ = client.Publish(ctx, "foo", "hello").Err() return ch - }, "15s", "100ms").Should(Receive(&msg)) + }, "20s", "100ms").Should(Receive(&msg)) Expect(msg.Channel).To(Equal("foo")) Expect(msg.Payload).To(Equal("hello")) Expect(sub.Close()).NotTo(HaveOccurred()) - _, err = startRedis(masterPort) - Expect(err).NotTo(HaveOccurred()) }) It("should sentinel cluster client setname", func() { - Skip("Flaky Test") err := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error { return c.Ping(ctx).Err() }) @@ -299,7 +290,6 @@ var _ = Describe("NewFailoverClusterClient", func() { }) It("should sentinel cluster PROTO 3", func() { - Skip("Flaky Test") _ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error { val, err := client.Do(ctx, "HELLO").Result() Expect(err).NotTo(HaveOccurred()) @@ -317,8 +307,8 @@ var _ = Describe("SentinelAclAuth", func() { var client *redis.Client var sentinel *redis.SentinelClient - sentinels := func() []*redisProcess { - return []*redisProcess{sentinel1, sentinel2, sentinel3} + sentinels := func() []*redis.Client { + return []*redis.Client{sentinel1, sentinel2, sentinel3} } BeforeEach(func() { @@ -328,7 +318,7 @@ var _ = Describe("SentinelAclAuth", func() { "+sentinel|myid", "+sentinel|replicas", "+sentinel|sentinels") for _, process := range sentinels() { - err := process.Client.Process(ctx, authCmd) + err := process.Process(ctx, authCmd) Expect(err).NotTo(HaveOccurred()) } @@ -356,7 +346,7 @@ var _ = Describe("SentinelAclAuth", func() { for _, process := range sentinels() { Eventually(func() string { return process.Info(ctx).Val() - }, "15s", "100ms").Should(ContainSubstring("sentinels=3")) + }, "20s", "100ms").Should(ContainSubstring("sentinels=3")) } }) @@ -364,7 +354,7 @@ var _ = Describe("SentinelAclAuth", func() { unauthCommand := redis.NewStatusCmd(ctx, "ACL", "DELUSER", aclSentinelUsername) for _, process := range sentinels() { - err := process.Client.Process(ctx, unauthCommand) + err := process.Process(ctx, unauthCommand) Expect(err).NotTo(HaveOccurred()) } diff --git a/timeseries_commands_test.go b/timeseries_commands_test.go index a2d4ba2936..d0d865b48c 100644 --- a/timeseries_commands_test.go +++ b/timeseries_commands_test.go @@ -43,6 +43,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { }) It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "older redis stack has different results for timeseries module") result, err := client.TSCreate(ctx, "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo("OK")) @@ -139,6 +140,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { {Timestamp: 1013, Value: 10.0}})) }) It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "older redis stack has different results for timeseries module") result, err := client.TSAdd(ctx, "1", 1, 1).Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo(1)) @@ -232,6 +234,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { }) It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "older redis stack has different results for timeseries module") result, err := client.TSCreate(ctx, "1").Result() Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeEquivalentTo("OK")) @@ -349,6 +352,7 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { }) It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "older redis stack has different results for timeseries module") for i := 0; i < 100; i++ { _, err := client.TSIncrBy(ctx, "1", 1).Result() Expect(err).NotTo(HaveOccurred()) diff --git a/universal_test.go b/universal_test.go index ba04324f43..2a1fac39d1 100644 --- a/universal_test.go +++ b/universal_test.go @@ -16,8 +16,7 @@ var _ = Describe("UniversalClient", func() { } }) - It("should connect to failover servers", func() { - Skip("Flaky Test") + It("should connect to failover servers", Label("NonRedisEnterprise"), func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ MasterName: sentinelName, Addrs: sentinelAddrs, From 57704a3616e687020246509fef4c788d053139a4 Mon Sep 17 00:00:00 2001 From: "fengyun.rui" Date: Tue, 4 Mar 2025 19:31:45 +0800 Subject: [PATCH 115/230] feat: add hstrlen command for hash (#2843) * feat: add hstrlen command for hash Signed-off-by: rfyiamcool * feat: add hstrlen command for hash Signed-off-by: rfyiamcool --------- Signed-off-by: rfyiamcool Co-authored-by: Nedyalko Dyakov --- commands_test.go | 18 ++++++++++++++++++ hash_commands.go | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/commands_test.go b/commands_test.go index 0bbc3688d8..681fe470d5 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2626,6 +2626,23 @@ var _ = Describe("Commands", func() { )) }) + It("should HStrLen", func() { + hSet := client.HSet(ctx, "hash", "key", "hello") + Expect(hSet.Err()).NotTo(HaveOccurred()) + + hStrLen := client.HStrLen(ctx, "hash", "key") + Expect(hStrLen.Err()).NotTo(HaveOccurred()) + Expect(hStrLen.Val()).To(Equal(int64(len("hello")))) + + nonHStrLen := client.HStrLen(ctx, "hash", "keyNon") + Expect(hStrLen.Err()).NotTo(HaveOccurred()) + Expect(nonHStrLen.Val()).To(Equal(int64(0))) + + hDel := client.HDel(ctx, "hash", "key") + Expect(hDel.Err()).NotTo(HaveOccurred()) + Expect(hDel.Val()).To(Equal(int64(1))) + }) + It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() @@ -2642,6 +2659,7 @@ var _ = Describe("Commands", func() { Expect(res).To(Equal([]int64{1, 1, -2})) }) + It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() diff --git a/hash_commands.go b/hash_commands.go index 6596c6f5f7..039d8e07e5 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -23,6 +23,7 @@ type HashCmdable interface { HVals(ctx context.Context, key string) *StringSliceCmd HRandField(ctx context.Context, key string, count int) *StringSliceCmd HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd + HStrLen(ctx context.Context, key, field string) *IntCmd HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd @@ -190,6 +191,11 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str return cmd } +func (c cmdable) HStrLen(ctx context.Context, key, field string) *IntCmd { + cmd := NewIntCmd(ctx, "hstrlen", key, field) + _ = c(ctx, cmd) + return cmd +} func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd { args := []interface{}{"hscan", key, cursor} if match != "" { From d3d98f1e967baadde590d051619ab4fcd641bba7 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 4 Mar 2025 14:28:08 +0200 Subject: [PATCH 116/230] fix(test): sort results in example test (#3292) --- doctests/query_range_test.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/doctests/query_range_test.go b/doctests/query_range_test.go index 41438ff0e5..09b3bb619e 100644 --- a/doctests/query_range_test.go +++ b/doctests/query_range_test.go @@ -230,6 +230,12 @@ func ExampleClient_query_range() { FieldName: "price", }, }, + SortBy: []redis.FTSearchSortBy{ + { + FieldName: "price", + Asc: true, + }, + }, }, ).Result() @@ -263,6 +269,12 @@ func ExampleClient_query_range() { FieldName: "price", }, }, + SortBy: []redis.FTSearchSortBy{ + { + FieldName: "price", + Asc: true, + }, + }, }, ).Result() @@ -289,6 +301,12 @@ func ExampleClient_query_range() { FieldName: "price", }, }, + SortBy: []redis.FTSearchSortBy{ + { + FieldName: "price", + Asc: true, + }, + }, Filters: []redis.FTSearchFilter{ { FieldName: "price", @@ -354,19 +372,19 @@ func ExampleClient_query_range() { // Output: // 3 - // bicycle:2 : price 815 // bicycle:5 : price 810 + // bicycle:2 : price 815 // bicycle:9 : price 815 // 3 - // bicycle:2 : price 815 // bicycle:5 : price 810 + // bicycle:2 : price 815 // bicycle:9 : price 815 // 5 // bicycle:1 : price 1200 - // bicycle:4 : price 3200 + // bicycle:8 : price 1200 // bicycle:6 : price 2300 + // bicycle:4 : price 3200 // bicycle:3 : price 3400 - // bicycle:8 : price 1200 // 7 // bicycle:0 : price 270 // bicycle:7 : price 430 From 5c5ddc93d99069ca4f6aebc4c7596a6c9c8714c5 Mon Sep 17 00:00:00 2001 From: Bhargav Dodla <13788369+EXPEbdodla@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:08:27 -0800 Subject: [PATCH 117/230] fix: Fixed issue with context cancelled error leading to connection spikes on Primary instances (#3190) * fix: Fixed issue with context cancelled error leading to connection spikes on Master * fix: Added tests * fix: Updated tests --------- Co-authored-by: Bhargav Dodla Co-authored-by: Nedyalko Dyakov --- error.go | 9 +++++++++ osscluster.go | 4 +++- osscluster_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/error.go b/error.go index 9b348193a4..a7bf159c24 100644 --- a/error.go +++ b/error.go @@ -38,6 +38,15 @@ type Error interface { var _ Error = proto.RedisError("") +func isContextError(err error) bool { + switch err { + case context.Canceled, context.DeadlineExceeded: + return true + default: + return false + } +} + func shouldRetry(err error, retryTimeout bool) bool { switch err { case io.EOF, io.ErrUnexpectedEOF: diff --git a/osscluster.go b/osscluster.go index 517fbd4506..1e9ee7de43 100644 --- a/osscluster.go +++ b/osscluster.go @@ -1350,7 +1350,9 @@ func (c *ClusterClient) processPipelineNode( _ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error { cn, err := node.Client.getConn(ctx) if err != nil { - node.MarkAsFailing() + if !isContextError(err) { + node.MarkAsFailing() + } _ = c.mapCmdsByNode(ctx, failedCmds, cmds) setCmdsErr(cmds, err) return err diff --git a/osscluster_test.go b/osscluster_test.go index aeb34c6bdc..ccf6daad8f 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -539,6 +539,39 @@ var _ = Describe("ClusterClient", func() { AfterEach(func() {}) assertPipeline() + + It("doesn't fail node with context.Canceled error", func() { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + pipe.Set(ctx, "A", "A_value", 0) + _, err := pipe.Exec(ctx) + + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, context.Canceled)).To(BeTrue()) + + clientNodes, _ := client.Nodes(ctx, "A") + + for _, node := range clientNodes { + Expect(node.Failing()).To(BeFalse()) + } + }) + + It("doesn't fail node with context.DeadlineExceeded error", func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + defer cancel() + + pipe.Set(ctx, "A", "A_value", 0) + _, err := pipe.Exec(ctx) + + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, context.DeadlineExceeded)).To(BeTrue()) + + clientNodes, _ := client.Nodes(ctx, "A") + + for _, node := range clientNodes { + Expect(node.Failing()).To(BeFalse()) + } + }) }) Describe("with TxPipeline", func() { From 7f141173f19e51eba4df5d4a595784e46365fc08 Mon Sep 17 00:00:00 2001 From: milad Date: Thu, 6 Mar 2025 18:23:14 +0330 Subject: [PATCH 118/230] add readOnly on failover opts (#3281) * add readOnly on failover opts add failover that if is True it connects to slaves * add test * add test for slave connect * fix test * fix tests * skip Flaky * add more tests on slave * add test * delete file * Update universal_test.go enable previously skipped test. * rm skip from sentinels * rm tmp files * Update universal_test.go don't run sentinel test in RE * Update universal_test.go --------- Co-authored-by: Nedyalko Dyakov --- universal.go | 2 ++ universal_test.go | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/universal.go b/universal.go index 21867ec224..3e3367e315 100644 --- a/universal.go +++ b/universal.go @@ -163,6 +163,8 @@ func (o *UniversalOptions) Failover() *FailoverOptions { TLSConfig: o.TLSConfig, + ReplicaOnly: o.ReadOnly, + DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, diff --git a/universal_test.go b/universal_test.go index 2a1fac39d1..e389fe4fb2 100644 --- a/universal_test.go +++ b/universal_test.go @@ -60,6 +60,25 @@ var _ = Describe("UniversalClient", func() { Expect(a).ToNot(Panic()) }) + It("should connect to failover servers on slaves when readonly Options is ok", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + MasterName: sentinelName, + Addrs: sentinelAddrs, + ReadOnly: true, + }) + Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) + + roleCmd := client.Do(ctx, "ROLE") + role, err := roleCmd.Result() + Expect(err).NotTo(HaveOccurred()) + + roleSlice, ok := role.([]interface{}) + Expect(ok).To(BeTrue()) + Expect(roleSlice[0]).To(Equal("slave")) + + err = client.Set(ctx, "somekey", "somevalue", 0).Err() + Expect(err).To(HaveOccurred()) + }) It("should connect to clusters if IsClusterMode is set even if only a single address is provided", Label("NonRedisEnterprise"), func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: []string{cluster.addrs()[0]}, @@ -77,3 +96,4 @@ var _ = Describe("UniversalClient", func() { Expect(client.ClusterSlots(ctx).Val()).To(HaveLen(3)) }) }) + From 6572f0fad39d71840f2e3a11078e62123f92ba3b Mon Sep 17 00:00:00 2001 From: David Maier <60782329+dmaier-redislabs@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:57:19 +0100 Subject: [PATCH 119/230] Update README.md Minor update on the README by highlighting that go-redis is the official Redis library. The note about Uptrace was moved to the 'contributors' section of the README. --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3ab23ba699..e8342734da 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,7 @@ [![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) [![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj) -> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). -> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can -> use it to monitor applications and set up automatic alerts to receive notifications via email, -> Slack, Telegram, and others. -> -> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which -> demonstrates how you can use Uptrace to monitor go-redis. +> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers. ## Supported versions @@ -297,6 +291,14 @@ REDIS_PORT=9999 go test ## Contributors +> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). +> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can +> use it to monitor applications and set up automatic alerts to receive notifications via email, +> Slack, Telegram, and others. +> +> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which +> demonstrates how you can use Uptrace to monitor go-redis. + Thanks to all the people who already contributed! From b6b2d7edbedc23db1f5ed52d26dd4023edb99b95 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:54:25 +0200 Subject: [PATCH 120/230] Enable dialect 2 on default (#3213) * Enable dialect 2 on deafult * add vector test for default dialect * Add dialect 1 test * Add dialect 1 test & fix ft.search * Add default dialect to Readme --- README.md | 4 ++++ search_commands.go | 12 ++++++++++++ search_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/README.md b/README.md index e8342734da..cfe97d3772 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,10 @@ res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptio val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal() ``` +#### Redis-Search Default Dialect + +In the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries. + ## Contributing Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! diff --git a/search_commands.go b/search_commands.go index c50ac07fb4..8be39d2a19 100644 --- a/search_commands.go +++ b/search_commands.go @@ -604,6 +604,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } else { + queryArgs = append(queryArgs, "DIALECT", 2) } } return queryArgs @@ -801,6 +803,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st } if options.DialectVersion > 0 { args = append(args, "DIALECT", options.DialectVersion) + } else { + args = append(args, "DIALECT", 2) } } @@ -1174,6 +1178,8 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri args := []interface{}{"FT.EXPLAIN", index, query} if options.Dialect != "" { args = append(args, "DIALECT", options.Dialect) + } else { + args = append(args, "DIALECT", 2) } cmd := NewStringCmd(ctx, args...) _ = c(ctx, cmd) @@ -1471,6 +1477,8 @@ func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query s } if options.Dialect > 0 { args = append(args, "DIALECT", options.Dialect) + } else { + args = append(args, "DIALECT", 2) } } cmd := newFTSpellCheckCmd(ctx, args...) @@ -1840,6 +1848,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { } if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } else { + queryArgs = append(queryArgs, "DIALECT", 2) } } return queryArgs @@ -1955,6 +1965,8 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin } if options.DialectVersion > 0 { args = append(args, "DIALECT", options.DialectVersion) + } else { + args = append(args, "DIALECT", 2) } } cmd := newFTSearchCmd(ctx, options, args...) diff --git a/search_test.go b/search_test.go index d309b1a8be..6b6425191d 100644 --- a/search_test.go +++ b/search_test.go @@ -1143,6 +1143,55 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) }) + It("should FTCreate VECTOR with dialect 1 ", Label("search", "ftcreate"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "v"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "v", Asc: true}}, + Limit: 10, + DialectVersion: 1, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["v"]).To(BeEquivalentTo("aaaaaaaa")) + }) + + It("should FTCreate VECTOR with default dialect", Label("search", "ftcreate"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) From f3979d7144eae8cf18423a0965d38f4d0ff3fe09 Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Thu, 13 Mar 2025 05:55:28 -0700 Subject: [PATCH 121/230] Set client name in HELLO RESP handshake (#3294) --- redis.go | 2 +- redis_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/redis.go b/redis.go index ec3ff616ac..533c63c3db 100644 --- a/redis.go +++ b/redis.go @@ -310,7 +310,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, protocol, username, password, "").Err(); err == nil { + if err = conn.Hello(ctx, protocol, username, password, c.opt.ClientName).Err(); err == nil { auth = true } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal diff --git a/redis_test.go b/redis_test.go index f8c91b4a22..27a24c9c37 100644 --- a/redis_test.go +++ b/redis_test.go @@ -186,6 +186,32 @@ var _ = Describe("Client", func() { Expect(val).Should(ContainSubstring("name=hi")) }) + It("should attempt to set client name in HELLO", func() { + opt := redisOptions() + opt.ClientName = "hi" + db := redis.NewClient(opt) + + defer func() { + Expect(db.Close()).NotTo(HaveOccurred()) + }() + + // Client name should be already set on any successfully initialized connection + name, err := db.ClientGetName(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(name).Should(Equal("hi")) + + // HELLO should be able to explicitly overwrite the client name + conn := db.Conn() + hello, err := conn.Hello(ctx, 3, "", "", "hi2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(hello["proto"]).Should(Equal(int64(3))) + name, err = conn.ClientGetName(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(name).Should(Equal("hi2")) + err = conn.Close() + Expect(err).NotTo(HaveOccurred()) + }) + It("should client PROTO 2", func() { opt := redisOptions() opt.Protocol = 2 From 28d0d08ce4f58e5024c7fff6e300ed01fd3cc3ee Mon Sep 17 00:00:00 2001 From: Monkey Date: Fri, 14 Mar 2025 16:05:22 +0800 Subject: [PATCH 122/230] fix: connection pool timeout, increase retries (#3298) * fix: connection pool timeout, increase retries Signed-off-by: monkey * fix: add shouldRetry test Signed-off-by: monkey --------- Signed-off-by: monkey Co-authored-by: Nedyalko Dyakov --- error.go | 3 +++ error_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++ export_test.go | 6 +++++ 3 files changed, 74 insertions(+) create mode 100644 error_test.go diff --git a/error.go b/error.go index a7bf159c24..ec2224c0dd 100644 --- a/error.go +++ b/error.go @@ -53,6 +53,9 @@ func shouldRetry(err error, retryTimeout bool) bool { return true case nil, context.Canceled, context.DeadlineExceeded: return false + case pool.ErrPoolTimeout: + // connection pool timeout, increase retries. #3289 + return true } if v, ok := err.(timeoutError); ok { diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000000..da9a471a28 --- /dev/null +++ b/error_test.go @@ -0,0 +1,65 @@ +package redis_test + +import ( + "context" + "errors" + "io" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +type testTimeout struct { + timeout bool +} + +func (t testTimeout) Timeout() bool { + return t.timeout +} + +func (t testTimeout) Error() string { + return "test timeout" +} + +var _ = Describe("error", func() { + BeforeEach(func() { + + }) + + AfterEach(func() { + + }) + + It("should retry", func() { + data := map[error]bool{ + io.EOF: true, + io.ErrUnexpectedEOF: true, + nil: false, + context.Canceled: false, + context.DeadlineExceeded: false, + redis.ErrPoolTimeout: true, + errors.New("ERR max number of clients reached"): true, + errors.New("LOADING Redis is loading the dataset in memory"): true, + errors.New("READONLY You can't write against a read only replica"): true, + errors.New("CLUSTERDOWN The cluster is down"): true, + errors.New("TRYAGAIN Command cannot be processed, please try again"): true, + errors.New("other"): false, + } + + for err, expected := range data { + Expect(redis.ShouldRetry(err, false)).To(Equal(expected)) + Expect(redis.ShouldRetry(err, true)).To(Equal(expected)) + } + }) + + It("should retry timeout", func() { + t1 := testTimeout{timeout: true} + Expect(redis.ShouldRetry(t1, true)).To(Equal(true)) + Expect(redis.ShouldRetry(t1, false)).To(Equal(false)) + + t2 := testTimeout{timeout: false} + Expect(redis.ShouldRetry(t2, true)).To(Equal(true)) + Expect(redis.ShouldRetry(t2, false)).To(Equal(true)) + }) +}) diff --git a/export_test.go b/export_test.go index 3f92983dd3..10d8f23ce5 100644 --- a/export_test.go +++ b/export_test.go @@ -11,6 +11,8 @@ import ( "github.com/redis/go-redis/v9/internal/pool" ) +var ErrPoolTimeout = pool.ErrPoolTimeout + func (c *baseClient) Pool() pool.Pooler { return c.connPool } @@ -102,3 +104,7 @@ func (c *Ring) ShardByName(name string) *ringShard { func (c *ModuleLoadexConfig) ToArgs() []interface{} { return c.toArgs() } + +func ShouldRetry(err error, retryTimeout bool) bool { + return shouldRetry(err, retryTimeout) +} From 3bb6b763c6e40db4ac5b0f9790a0c4e8f37fc015 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:27:26 +0200 Subject: [PATCH 123/230] chore(deps): bump golangci/golangci-lint-action from 6.5.0 to 6.5.1 (#3302) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.0 to 6.5.1. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.5.0...v6.5.1) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d9e53f706e..7eeddefbc0 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,4 +21,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.5.0 + uses: golangci/golangci-lint-action@v6.5.1 From cc3b6e71edb6bde41844c4c30cce85fe37d277e4 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 18 Mar 2025 14:20:18 +0200 Subject: [PATCH 124/230] chore: use redis 8 ce m05 (#3304) * use m05 * add platform, since client-libs has image for amd64 * verbose testing * Update .github/workflows/build.yml --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- Makefile | 2 +- docker-compose.yml | 5 +++++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index a1b96d88b6..def48baf8f 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M03"]="8.0-M04-pre" + ["8.0-M05"]="8.0-M05-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 592e487643..48bbdb7510 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M03" # 8.0 milestone 4 + - "8.0-M05" # 8.0 milestone 5 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M03"]="8.0-M04-pre" + ["8.0-M05"]="8.0-M05-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M03" # 8.0 milestone 4 + - "8.0-M05" # 8.0 milestone 5 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/Makefile b/Makefile index d94676ad49..fc175f5f18 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ test.ci: (cd "$${dir}" && \ go mod tidy -compat=1.18 && \ go vet && \ - go test -coverprofile=coverage.txt -covermode=atomic ./... -race); \ + go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race); \ done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet diff --git a/docker-compose.yml b/docker-compose.yml index 5bf69f19d0..3d4347bf21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: redis: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -23,6 +24,7 @@ services: osscluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-osscluster environment: - NODES=6 @@ -39,6 +41,7 @@ services: sentinel-cluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-sentinel-cluster network_mode: "host" environment: @@ -58,6 +61,7 @@ services: sentinel: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-sentinel depends_on: - sentinel-cluster @@ -81,6 +85,7 @@ services: ring-cluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-ring-cluster environment: - NODES=3 From f1fa67a1a2d1be49525a0aad42c18c4dad606213 Mon Sep 17 00:00:00 2001 From: b1ackd0t <28790446+rodneyosodo@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:50:29 +0300 Subject: [PATCH 125/230] fix(tracing): show the whole command rather than truncated version of it (#3290) Truncate version of a long key might not be useful when debugging Signed-off-by: Rodney Osodo Co-authored-by: Nedyalko Dyakov --- extra/rediscmd/rediscmd.go | 18 ---- extra/redisotel/tracing_test.go | 164 ++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 18 deletions(-) diff --git a/extra/rediscmd/rediscmd.go b/extra/rediscmd/rediscmd.go index c97689f95c..6423b6abdb 100644 --- a/extra/rediscmd/rediscmd.go +++ b/extra/rediscmd/rediscmd.go @@ -17,7 +17,6 @@ func CmdString(cmd redis.Cmder) string { } func CmdsString(cmds []redis.Cmder) (string, string) { - const numCmdLimit = 100 const numNameLimit = 10 seen := make(map[string]struct{}, numNameLimit) @@ -26,10 +25,6 @@ func CmdsString(cmds []redis.Cmder) (string, string) { b := make([]byte, 0, 32*len(cmds)) for i, cmd := range cmds { - if i > numCmdLimit { - break - } - if i > 0 { b = append(b, '\n') } @@ -51,12 +46,7 @@ func CmdsString(cmds []redis.Cmder) (string, string) { } func AppendCmd(b []byte, cmd redis.Cmder) []byte { - const numArgLimit = 32 - for i, arg := range cmd.Args() { - if i > numArgLimit { - break - } if i > 0 { b = append(b, ' ') } @@ -72,20 +62,12 @@ func AppendCmd(b []byte, cmd redis.Cmder) []byte { } func appendArg(b []byte, v interface{}) []byte { - const argLenLimit = 64 - switch v := v.(type) { case nil: return append(b, ""...) case string: - if len(v) > argLenLimit { - v = v[:argLenLimit] - } return appendUTF8String(b, Bytes(v)) case []byte: - if len(v) > argLenLimit { - v = v[:argLenLimit] - } return appendUTF8String(b, v) case int: return strconv.AppendInt(b, int64(v), 10) diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go index bbe8281440..e5ef86edcc 100644 --- a/extra/redisotel/tracing_test.go +++ b/extra/redisotel/tracing_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strings" "testing" "go.opentelemetry.io/otel/attribute" @@ -222,6 +223,169 @@ func TestTracingHook_ProcessPipelineHook(t *testing.T) { } } +func TestTracingHook_ProcessHook_LongCommand(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + longValue := strings.Repeat("a", 102400) + + tests := []struct { + name string + cmd redis.Cmder + expected string + }{ + { + name: "short command", + cmd: redis.NewCmd(context.Background(), "SET", "key", "value"), + expected: "SET key value", + }, + { + name: "set command with long key", + cmd: redis.NewCmd(context.Background(), "SET", longValue, "value"), + expected: "SET " + longValue + " value", + }, + { + name: "set command with long value", + cmd: redis.NewCmd(context.Background(), "SET", "key", longValue), + expected: "SET key " + longValue, + }, + { + name: "set command with long key and value", + cmd: redis.NewCmd(context.Background(), "SET", longValue, longValue), + expected: "SET " + longValue + " " + longValue, + }, + { + name: "short command with many arguments", + cmd: redis.NewCmd(context.Background(), "MSET", "key1", "value1", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5"), + expected: "MSET key1 value1 key2 value2 key3 value3 key4 value4 key5 value5", + }, + { + name: "long command", + cmd: redis.NewCmd(context.Background(), longValue, "key", "value"), + expected: longValue + " key value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + return nil + }) + + if err := processHook(context.Background(), tt.cmd); err != nil { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + + var dbStatement string + for _, attr := range spanData.Attributes { + if attr.Key == semconv.DBStatementKey { + dbStatement = attr.Value.AsString() + break + } + } + + if dbStatement != tt.expected { + t.Errorf("Expected DB statement: %q\nGot: %q", tt.expected, dbStatement) + } + }) + } +} + +func TestTracingHook_ProcessPipelineHook_LongCommands(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + cmds []redis.Cmder + expected string + }{ + { + name: "multiple short commands", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", "key1", "value1"), + redis.NewCmd(context.Background(), "SET", "key2", "value2"), + }, + expected: "SET key1 value1\nSET key2 value2", + }, + { + name: "multiple short commands with long key", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", strings.Repeat("a", 102400), "value1"), + redis.NewCmd(context.Background(), "SET", strings.Repeat("b", 102400), "value2"), + }, + expected: "SET " + strings.Repeat("a", 102400) + " value1\nSET " + strings.Repeat("b", 102400) + " value2", + }, + { + name: "multiple short commands with long value", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", "key1", strings.Repeat("a", 102400)), + redis.NewCmd(context.Background(), "SET", "key2", strings.Repeat("b", 102400)), + }, + expected: "SET key1 " + strings.Repeat("a", 102400) + "\nSET key2 " + strings.Repeat("b", 102400), + }, + { + name: "multiple short commands with long key and value", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", strings.Repeat("a", 102400), strings.Repeat("b", 102400)), + redis.NewCmd(context.Background(), "SET", strings.Repeat("c", 102400), strings.Repeat("d", 102400)), + }, + expected: "SET " + strings.Repeat("a", 102400) + " " + strings.Repeat("b", 102400) + "\nSET " + strings.Repeat("c", 102400) + " " + strings.Repeat("d", 102400), + }, + { + name: "multiple long commands", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), strings.Repeat("a", 102400), "key1", "value1"), + redis.NewCmd(context.Background(), strings.Repeat("a", 102400), "key2", "value2"), + }, + expected: strings.Repeat("a", 102400) + " key1 value1\n" + strings.Repeat("a", 102400) + " key2 value2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + processHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error { + return nil + }) + + if err := processHook(context.Background(), tt.cmds); err != nil { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + + var dbStatement string + for _, attr := range spanData.Attributes { + if attr.Key == semconv.DBStatementKey { + dbStatement = attr.Value.AsString() + break + } + } + + if dbStatement != tt.expected { + t.Errorf("Expected DB statement:\n%q\nGot:\n%q", tt.expected, dbStatement) + } + }) + } +} + func assertEqual(t *testing.T, expected, actual interface{}) { t.Helper() if expected != actual { From 94288e80a4f5d15cf8d65873dc579ab62191680b Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:05:45 +0200 Subject: [PATCH 126/230] Add vector types INT8 and UINT8 test (#3299) --- search_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/search_test.go b/search_test.go index 6b6425191d..296f5bd8b4 100644 --- a/search_test.go +++ b/search_test.go @@ -1626,6 +1626,63 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2")) }) + + It("should FTCreate VECTOR with int8 and uint8 types", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(7.9, "doesn't work with older redis") + // Define INT8 vector field + hnswOptionsInt8 := &redis.FTHNSWOptions{ + Type: "INT8", + Dim: 2, + DistanceMetric: "L2", + } + + // Define UINT8 vector field + hnswOptionsUint8 := &redis.FTHNSWOptions{ + Type: "UINT8", + Dim: 2, + DistanceMetric: "L2", + } + + // Create index with INT8 and UINT8 vector fields + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "int8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsInt8}}, + &redis.FieldSchema{FieldName: "uint8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsUint8}}, + ).Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // Insert vectors in int8 and uint8 format + client.HSet(ctx, "doc1", "int8_vector", "\x01\x02", "uint8_vector", "\x01\x02") + client.HSet(ctx, "doc2", "int8_vector", "\x03\x04", "uint8_vector", "\x03\x04") + + // Perform KNN search on INT8 vector + searchOptionsInt8 := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "int8_vector"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "int8_vector", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "\x01\x02"}, + } + + resInt8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @int8_vector $vec]", searchOptionsInt8).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resInt8.Docs[0].ID).To(BeEquivalentTo("doc1")) + + // Perform KNN search on UINT8 vector + searchOptionsUint8 := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "uint8_vector"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "uint8_vector", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "\x01\x02"}, + } + + resUint8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @uint8_vector $vec]", searchOptionsUint8).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) + }) + }) func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { From 2ff957aeb0edcf4248e76d6a2cc20b3205df8007 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:36:58 +0000 Subject: [PATCH 127/230] DOC-4494 SADD and SMEMBERS command examples (#3242) * DOC-4494 added sadd example * DOC-4494 sadd and smembers examples * DOC-4494 better naming convention for result variables --------- Co-authored-by: Nedyalko Dyakov --- doctests/cmds_set_test.go | 102 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 doctests/cmds_set_test.go diff --git a/doctests/cmds_set_test.go b/doctests/cmds_set_test.go new file mode 100644 index 0000000000..fecddbb831 --- /dev/null +++ b/doctests/cmds_set_test.go @@ -0,0 +1,102 @@ +// EXAMPLE: cmds_set +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_sadd_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myset") + // REMOVE_END + + // STEP_START sadd + sAddResult1, err := rdb.SAdd(ctx, "myset", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult1) // >>> 1 + + sAddResult2, err := rdb.SAdd(ctx, "myset", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult2) // >>> 1 + + sAddResult3, err := rdb.SAdd(ctx, "myset", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult3) // >>> 0 + + sMembersResult, err := rdb.SMembers(ctx, "myset").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sMembersResult) // >>> [Hello World] + // STEP_END + + // Output: + // 1 + // 1 + // 0 + // [Hello World] +} + +func ExampleClient_smembers_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myset") + // REMOVE_END + + // STEP_START smembers + sAddResult, err := rdb.SAdd(ctx, "myset", "Hello", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult) // >>> 2 + + sMembersResult, err := rdb.SMembers(ctx, "myset").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sMembersResult) // >>> [Hello World] + // STEP_END + + // Output: + // 2 + // [Hello World] +} From a8e0b2637f42eb834c384f4075aeabbc45576beb Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Wed, 19 Mar 2025 19:02:36 +0200 Subject: [PATCH 128/230] fix: handle network error on SETINFO (#3295) (CVE-2025-29923) * fix: handle network error on SETINFO This fix addresses potential out of order responses as described in `CVE-2025-29923` * fix: deprecate DisableIndentity and introduce DisableIdentity Both options will work before V10. In v10 DisableIndentity will be dropped. The preferred flag to use is `DisableIdentity`. --- README.md | 8 +++++--- bench_decode_test.go | 4 ++-- options.go | 11 ++++++++++- osscluster.go | 18 +++++++++++++++--- redis.go | 8 ++++++-- redis_test.go | 7 +++++++ ring.go | 20 ++++++++++++++++---- sentinel.go | 31 ++++++++++++++++++++++++------- universal.go | 25 +++++++++++++++++++------ 9 files changed, 104 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index cfe97d3772..335d32dad7 100644 --- a/README.md +++ b/README.md @@ -178,16 +178,18 @@ By default, go-redis automatically sends the client library name and version dur #### Disabling Identity Verification -When connection identity verification is not required or needs to be explicitly disabled, a `DisableIndentity` configuration option exists. In V10 of this library, `DisableIndentity` will become `DisableIdentity` in order to fix the associated typo. +When connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists. +Initially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library. +Although both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues. -To disable verification, set the `DisableIndentity` option to `true` in the Redis client options: +To disable verification, set the `DisableIdentity` option to `true` in the Redis client options: ```go rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, - DisableIndentity: true, // Disable set-info on connect + DisableIdentity: true, // Disable set-info on connect }) ``` diff --git a/bench_decode_test.go b/bench_decode_test.go index 16bdf2cd34..d61a901a08 100644 --- a/bench_decode_test.go +++ b/bench_decode_test.go @@ -30,7 +30,7 @@ func NewClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, - DisableIndentity: true, + DisableIdentity: true, }) return stub } @@ -46,7 +46,7 @@ func NewClusterClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, - DisableIndentity: true, + DisableIdentity: true, ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) { return []ClusterSlot{ diff --git a/options.go b/options.go index a350a02f9b..c572bbe780 100644 --- a/options.go +++ b/options.go @@ -148,9 +148,18 @@ type Options struct { // Enables read only queries on slave/follower nodes. readOnly bool - // Disable set-lib on connect. Default is false. + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + // Add suffix to client name. Default is empty. IdentitySuffix string diff --git a/osscluster.go b/osscluster.go index 1e9ee7de43..b018cc9e46 100644 --- a/osscluster.go +++ b/osscluster.go @@ -90,8 +90,19 @@ type ClusterOptions struct { ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration - TLSConfig *tls.Config - DisableIndentity bool // Disable set-lib on connect. Default is false. + TLSConfig *tls.Config + + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. + DisableIndentity bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool IdentitySuffix string // Add suffix to client name. Default is empty. @@ -303,7 +314,8 @@ func (opt *ClusterOptions) clientOptions() *Options { MaxActiveConns: opt.MaxActiveConns, ConnMaxIdleTime: opt.ConnMaxIdleTime, ConnMaxLifetime: opt.ConnMaxLifetime, - DisableIndentity: opt.DisableIndentity, + DisableIdentity: opt.DisableIdentity, + DisableIndentity: opt.DisableIdentity, IdentitySuffix: opt.IdentitySuffix, TLSConfig: opt.TLSConfig, // If ClusterSlots is populated, then we probably have an artificial diff --git a/redis.go b/redis.go index 533c63c3db..e0159294dd 100644 --- a/redis.go +++ b/redis.go @@ -350,7 +350,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return err } - if !c.opt.DisableIndentity { + if !c.opt.DisableIdentity && !c.opt.DisableIndentity { libName := "" libVer := Version() if c.opt.IdentitySuffix != "" { @@ -359,7 +359,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { p := conn.Pipeline() p.ClientSetInfo(ctx, WithLibraryName(libName)) p.ClientSetInfo(ctx, WithLibraryVersion(libVer)) - _, _ = p.Exec(ctx) + // Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid + // out of order responses later on. + if _, err = p.Exec(ctx); err != nil && !isRedisError(err) { + return err + } } if c.opt.OnConnect != nil { diff --git a/redis_test.go b/redis_test.go index 27a24c9c37..7d9bf1cef9 100644 --- a/redis_test.go +++ b/redis_test.go @@ -396,6 +396,13 @@ var _ = Describe("Client timeout", func() { }) testTimeout := func() { + It("SETINFO timeouts", func() { + conn := client.Conn() + err := conn.Ping(ctx).Err() + Expect(err).To(HaveOccurred()) + Expect(err.(net.Error).Timeout()).To(BeTrue()) + }) + It("Ping timeouts", func() { err := client.Ping(ctx).Err() Expect(err).To(HaveOccurred()) diff --git a/ring.go b/ring.go index 06a26020a8..0ff3f75b1e 100644 --- a/ring.go +++ b/ring.go @@ -98,9 +98,19 @@ type RingOptions struct { TLSConfig *tls.Config Limiter Limiter + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + IdentitySuffix string + UnstableResp3 bool } func (opt *RingOptions) init() { @@ -167,9 +177,11 @@ func (opt *RingOptions) clientOptions() *Options { TLSConfig: opt.TLSConfig, Limiter: opt.Limiter, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } diff --git a/sentinel.go b/sentinel.go index 3156955445..a4c9f53c40 100644 --- a/sentinel.go +++ b/sentinel.go @@ -80,9 +80,20 @@ type FailoverOptions struct { TLSConfig *tls.Config + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + + IdentitySuffix string + UnstableResp3 bool } func (opt *FailoverOptions) clientOptions() *Options { @@ -118,9 +129,11 @@ func (opt *FailoverOptions) clientOptions() *Options { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } @@ -156,9 +169,11 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } @@ -197,8 +212,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, + + IdentitySuffix: opt.IdentitySuffix, } } diff --git a/universal.go b/universal.go index 3e3367e315..0a25bf221b 100644 --- a/universal.go +++ b/universal.go @@ -61,14 +61,24 @@ type UniversalOptions struct { RouteByLatency bool RouteRandomly bool - // The sentinel master name. - // Only failover clients. - + // MasterName is the sentinel master name. + // Only for failover clients. MasterName string + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + + IdentitySuffix string + UnstableResp3 bool // IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint). IsClusterMode bool @@ -116,6 +126,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { TLSConfig: o.TLSConfig, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, @@ -163,8 +174,9 @@ func (o *UniversalOptions) Failover() *FailoverOptions { TLSConfig: o.TLSConfig, - ReplicaOnly: o.ReadOnly, + ReplicaOnly: o.ReadOnly, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, @@ -209,6 +221,7 @@ func (o *UniversalOptions) Simple() *Options { TLSConfig: o.TLSConfig, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, From cfa9ab9eff803688deea51740895bc2d5cc3bc45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:11:14 +0200 Subject: [PATCH 129/230] chore(deps): bump golang.org/x/net in /example/otel (#3308) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0. - [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- example/otel/go.mod | 7 ++++--- example/otel/go.sum | 20 ++++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/example/otel/go.mod b/example/otel/go.mod index 93b5d46c25..82ed7ed7a5 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -1,6 +1,7 @@ module github.com/redis/go-redis/example/otel go 1.19 +toolchain go1.24.1 replace github.com/redis/go-redis/v9 => ../.. @@ -34,9 +35,9 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index 1a1729c6ef..fa94c15b6f 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -1,10 +1,13 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -17,10 +20,13 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M= github.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM= go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc= @@ -46,12 +52,13 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= @@ -66,3 +73,4 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 70880f2592376959ceeaa307731b8cfc42efbf27 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 21 Mar 2025 13:13:14 +0200 Subject: [PATCH 130/230] release 9.7.3, retract 9.7.2 (#3314) (#3315) --- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 9 +++++---- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 9 ++++++--- extra/rediscmd/go.mod | 7 +++++-- extra/redisotel/go.mod | 9 ++++++--- extra/redisprometheus/go.mod | 7 +++++-- go.mod | 1 + version.go | 2 +- 12 files changed, 34 insertions(+), 20 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 40ad6297fb..0699671c59 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 14a8827fd1..72ab52aaf2 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 64f5c8af15..e2bc161a4f 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 82ed7ed7a5..299e4b02c2 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -1,6 +1,7 @@ module github.com/redis/go-redis/example/otel -go 1.19 +go 1.23.0 + toolchain go1.24.1 replace github.com/redis/go-redis/v9 => ../.. @@ -10,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -24,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index a973cd1790..044945654c 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 21d7e527d7..f44536075a 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index cc0bd0fb5f..11230414ba 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 go.opencensus.io v0.24.0 ) @@ -18,4 +18,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 0689fe904d..d64ad57011 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( @@ -15,4 +15,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index ab6288dec2..13899eb579 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 @@ -23,4 +23,7 @@ require ( golang.org/x/sys v0.16.0 // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index a1659bb0f6..fa3c43ae05 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( @@ -22,4 +22,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/go.mod b/go.mod index 1492d27098..83e8fd3d6d 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) retract ( + v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) diff --git a/version.go b/version.go index a447a546de..a4832fc1e0 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.7.1" + return "9.7.3" } From d6c22708e77c52d1ba461e5f6678fc127d101b80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 13:47:29 +0200 Subject: [PATCH 131/230] chore(deps): bump golangci/golangci-lint-action from 6.5.1 to 6.5.2 (#3317) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.5.1 to 6.5.2. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v6.5.1...v6.5.2) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 7eeddefbc0..68d906e131 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,4 +21,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.5.1 + uses: golangci/golangci-lint-action@v6.5.2 From 732a7b618c7393d664db81bc01a7e7f63bd860be Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:03:26 +0200 Subject: [PATCH 132/230] Support new hash commands: HGETDEL, HGETEX, HSETEX (#3305) --- commands_test.go | 143 ++++++++++++++++++++++++++++++++++++- example/hset-struct/go.sum | 2 - hash_commands.go | 116 +++++++++++++++++++++++++++++- 3 files changed, 257 insertions(+), 4 deletions(-) diff --git a/commands_test.go b/commands_test.go index 681fe470d5..a9e90fc993 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2659,7 +2659,6 @@ var _ = Describe("Commands", func() { Expect(res).To(Equal([]int64{1, 1, -2})) }) - It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() @@ -2812,6 +2811,148 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1)) }) + + It("should HGETDEL", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2", "f3", "val3").Err() + Expect(err).NotTo(HaveOccurred()) + + // Execute HGETDEL on fields f1 and f2. + res, err := client.HGetDel(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + // Expect the returned values for f1 and f2. + Expect(res).To(Equal([]string{"val1", "val2"})) + + // Verify that f1 and f2 have been deleted, while f3 remains. + remaining, err := client.HMGet(ctx, "myhash", "f1", "f2", "f3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(remaining[0]).To(BeNil()) + Expect(remaining[1]).To(BeNil()) + Expect(remaining[2]).To(Equal("val3")) + }) + + It("should return nil responses for HGETDEL on non-existent key", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + // HGETDEL on a key that does not exist. + res, err := client.HGetDel(ctx, "nonexistent", "f1", "f2").Result() + Expect(err).To(BeNil()) + Expect(res).To(Equal([]string{"", ""})) + }) + + // ----------------------------- + // HGETEX with various TTL options + // ----------------------------- + It("should HGETEX with EX option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with EX option and 60 seconds TTL. + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with PERSIST option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with PERSIST (no TTL value needed). + opt := redis.HGetEXOptions{ExpirationType: redis.HGetEXExpirationPERSIST} + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with EXAT option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Set expiration at a specific Unix timestamp (60 seconds from now). + expireAt := time.Now().Add(60 * time.Second).Unix() + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEXAT, + ExpirationVal: expireAt, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + // ----------------------------- + // HSETEX with FNX/FXX options + // ----------------------------- + It("should HSETEX with FNX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with FXX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f2", "val1").Err() + Expect(err).NotTo(HaveOccurred()) + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f3", "val3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with multiple field operations", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1", "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + values, err := client.HMGet(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal([]interface{}{"val1", "val2"})) + }) }) Describe("hyperloglog", func() { diff --git a/example/hset-struct/go.sum b/example/hset-struct/go.sum index 1602e702e5..5496d29e58 100644 --- a/example/hset-struct/go.sum +++ b/example/hset-struct/go.sum @@ -1,7 +1,5 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/hash_commands.go b/hash_commands.go index 039d8e07e5..1f53f344d9 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -10,13 +10,17 @@ type HashCmdable interface { HExists(ctx context.Context, key, field string) *BoolCmd HGet(ctx context.Context, key, field string) *StringCmd HGetAll(ctx context.Context, key string) *MapStringStringCmd - HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd + HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd HKeys(ctx context.Context, key string) *StringSliceCmd HLen(ctx context.Context, key string) *IntCmd HMGet(ctx context.Context, key string, fields ...string) *SliceCmd HSet(ctx context.Context, key string, values ...interface{}) *IntCmd HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd + HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd + HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd @@ -454,3 +458,113 @@ func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSl _ = c(ctx, cmd) return cmd } + +func (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETDEL", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ExpirationType represents an expiration option for the HGETEX command. +type HGetEXExpirationType string + +const ( + HGetEXExpirationEX HGetEXExpirationType = "EX" + HGetEXExpirationPX HGetEXExpirationType = "PX" + HGetEXExpirationEXAT HGetEXExpirationType = "EXAT" + HGetEXExpirationPXAT HGetEXExpirationType = "PXAT" + HGetEXExpirationPERSIST HGetEXExpirationType = "PERSIST" +) + +type HGetEXOptions struct { + ExpirationType HGetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key} + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HGetEXExpirationPERSIST { + args = append(args, options.ExpirationVal) + } + } + + args = append(args, "FIELDS", len(fields)) + for _, field := range fields { + args = append(args, field) + } + + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type HSetEXCondition string + +const ( + HSetEXFNX HSetEXCondition = "FNX" // Only set the fields if none of them already exist. + HSetEXFXX HSetEXCondition = "FXX" // Only set the fields if all already exist. +) + +type HSetEXExpirationType string + +const ( + HSetEXExpirationEX HSetEXExpirationType = "EX" + HSetEXExpirationPX HSetEXExpirationType = "PX" + HSetEXExpirationEXAT HSetEXExpirationType = "EXAT" + HSetEXExpirationPXAT HSetEXExpirationType = "PXAT" + HSetEXExpirationKEEPTTL HSetEXExpirationType = "KEEPTTL" +) + +type HSetEXOptions struct { + Condition HSetEXCondition + ExpirationType HSetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key, "FIELDS", len(fieldsAndValues) / 2} + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key} + if options.Condition != "" { + args = append(args, string(options.Condition)) + } + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HSetEXExpirationKEEPTTL { + args = append(args, options.ExpirationVal) + } + } + args = append(args, "FIELDS", len(fieldsAndValues)/2) + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} From 2ad1eb1225f399d3b3e63f52ef5c049c2e01b62e Mon Sep 17 00:00:00 2001 From: Alexander Menshchikov Date: Mon, 24 Mar 2025 15:21:08 +0300 Subject: [PATCH 133/230] Add FailoverClusterClient support for Universal client (#2794) * Add FailoverClusterClient support + fix example/hset-struct go.sum * Improve NewUniversalClient comment --------- Co-authored-by: Nedyalko Dyakov --- universal.go | 23 +++++++++++++++++------ universal_test.go | 12 +++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/universal.go b/universal.go index 0a25bf221b..3d91dd493e 100644 --- a/universal.go +++ b/universal.go @@ -154,6 +154,9 @@ func (o *UniversalOptions) Failover() *FailoverOptions { SentinelUsername: o.SentinelUsername, SentinelPassword: o.SentinelPassword, + RouteByLatency: o.RouteByLatency, + RouteRandomly: o.RouteRandomly, + MaxRetries: o.MaxRetries, MinRetryBackoff: o.MinRetryBackoff, MaxRetryBackoff: o.MaxRetryBackoff, @@ -256,14 +259,22 @@ var ( // NewUniversalClient returns a new multi client. The type of the returned client depends // on the following conditions: // -// 1. If the MasterName option is specified, a sentinel-backed FailoverClient is returned. -// 2. if the number of Addrs is two or more, a ClusterClient is returned. -// 3. Otherwise, a single-node Client is returned. +// 1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode, +// a FailoverClusterClient is returned. +// 2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode, +// a sentinel-backed FailoverClient is returned. +// 3. If the number of Addrs is two or more, or IsClusterMode option is specified, +// a ClusterClient is returned. +// 4. Otherwise, a single-node Client is returned. func NewUniversalClient(opts *UniversalOptions) UniversalClient { - if opts.MasterName != "" { + switch { + case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode): + return NewFailoverClusterClient(opts.Failover()) + case opts.MasterName != "": return NewFailoverClient(opts.Failover()) - } else if len(opts.Addrs) > 1 || opts.IsClusterMode { + case len(opts.Addrs) > 1 || opts.IsClusterMode: return NewClusterClient(opts.Cluster()) + default: + return NewClient(opts.Simple()) } - return NewClient(opts.Simple()) } diff --git a/universal_test.go b/universal_test.go index e389fe4fb2..f965253fc1 100644 --- a/universal_test.go +++ b/universal_test.go @@ -24,6 +24,16 @@ var _ = Describe("UniversalClient", func() { Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) }) + It("should connect to failover cluster", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + MasterName: sentinelName, + RouteRandomly: true, + Addrs: sentinelAddrs, + }) + _, ok := client.(*redis.ClusterClient) + Expect(ok).To(BeTrue(), "expected a ClusterClient") + }) + It("should connect to simple servers", func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: []string{redisAddr}, @@ -79,6 +89,7 @@ var _ = Describe("UniversalClient", func() { err = client.Set(ctx, "somekey", "somevalue", 0).Err() Expect(err).To(HaveOccurred()) }) + It("should connect to clusters if IsClusterMode is set even if only a single address is provided", Label("NonRedisEnterprise"), func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: []string{cluster.addrs()[0]}, @@ -96,4 +107,3 @@ var _ = Describe("UniversalClient", func() { Expect(client.ClusterSlots(ctx).Val()).To(HaveLen(3)) }) }) - From ba1f754a975ed8d590009f72b6de6a67f009300f Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 24 Mar 2025 15:19:26 +0200 Subject: [PATCH 134/230] chore: disable verifying golangci configuration (#3319) --- .github/workflows/golangci-lint.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 68d906e131..515750af60 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,3 +22,6 @@ jobs: - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v6.5.2 + with: + verify: false # disable verifying the configuration since golangci is currently introducing breaking changes in the configuration + From ad77123c9b36fedcbb2e9c11b42af01a739cc952 Mon Sep 17 00:00:00 2001 From: Justin <8886628+justinmir@users.noreply.github.com> Date: Mon, 24 Mar 2025 06:28:20 -0700 Subject: [PATCH 135/230] Make MASTERDOWN a retriable error in RedisCluster client (#3164) When clusters are running with `replica-server-stale-data no`, replicas will return a MASTERDOWN error under two conditions: 1. The primary has failed and we are not serving requests. 2. A replica has just started and has not yet synced from the primary. The former, primary has failed and we are not serving requests, is similar to a CLUSTERDOWN error and should be similarly retriable. When a replica has just started and has not yet synced from the primary the request should be retried on other available nodes in the shard. Otherwise a percentage of the read requests to the shard will fail. Examples when `replica-server-stale-data no` is enabled: 1. In a cluster using `ReadOnly` with a single read replica, every read request will return errors to the client because MASTERDOWN is not a retriable error. 2. In a cluster using `RouteRandomly` a percentage of the requests will return errors to the client based on if this server was selected. Co-authored-by: Nedyalko Dyakov --- error.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/error.go b/error.go index ec2224c0dd..6f47f7cf2c 100644 --- a/error.go +++ b/error.go @@ -75,6 +75,9 @@ func shouldRetry(err error, retryTimeout bool) bool { if strings.HasPrefix(s, "READONLY ") { return true } + if strings.HasPrefix(s, "MASTERDOWN ") { + return true + } if strings.HasPrefix(s, "CLUSTERDOWN ") { return true } From de418681952c321c2d827e441c170b74b988c299 Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Mon, 24 Mar 2025 06:45:43 -0700 Subject: [PATCH 136/230] Bound connection pool background dials to configured dial timeout (#3089) --- internal/pool/bench_test.go | 2 ++ internal/pool/pool.go | 12 ++++++++++-- internal/pool/pool_test.go | 5 +++++ options.go | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/pool/bench_test.go b/internal/pool/bench_test.go index 71049f480d..72308e1242 100644 --- a/internal/pool/bench_test.go +++ b/internal/pool/bench_test.go @@ -33,6 +33,7 @@ func BenchmarkPoolGetPut(b *testing.B) { Dialer: dummyDialer, PoolSize: bm.poolSize, PoolTimeout: time.Second, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Hour, }) @@ -76,6 +77,7 @@ func BenchmarkPoolGetRemove(b *testing.B) { Dialer: dummyDialer, PoolSize: bm.poolSize, PoolTimeout: time.Second, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Hour, }) diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 2125f3e133..b69c75f4f0 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -62,6 +62,7 @@ type Options struct { PoolFIFO bool PoolSize int + DialTimeout time.Duration PoolTimeout time.Duration MinIdleConns int MaxIdleConns int @@ -140,7 +141,10 @@ func (p *ConnPool) checkMinIdleConns() { } func (p *ConnPool) addIdleConn() error { - cn, err := p.dialConn(context.TODO(), true) + ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + defer cancel() + + cn, err := p.dialConn(ctx, true) if err != nil { return err } @@ -230,15 +234,19 @@ func (p *ConnPool) tryDial() { return } - conn, err := p.cfg.Dialer(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + + conn, err := p.cfg.Dialer(ctx) if err != nil { p.setLastDialError(err) time.Sleep(time.Second) + cancel() continue } atomic.StoreUint32(&p.dialErrorsNum, 0) _ = conn.Close() + cancel() return } } diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 4ccc489306..99f31bd74d 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -22,6 +22,7 @@ var _ = Describe("ConnPool", func() { Dialer: dummyDialer, PoolSize: 10, PoolTimeout: time.Hour, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, }) }) @@ -46,6 +47,7 @@ var _ = Describe("ConnPool", func() { }, PoolSize: 10, PoolTimeout: time.Hour, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, MinIdleConns: minIdleConns, }) @@ -129,6 +131,7 @@ var _ = Describe("MinIdleConns", func() { PoolSize: poolSize, MinIdleConns: minIdleConns, PoolTimeout: 100 * time.Millisecond, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: -1, }) Eventually(func() int { @@ -306,6 +309,7 @@ var _ = Describe("race", func() { Dialer: dummyDialer, PoolSize: 10, PoolTimeout: time.Minute, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, }) @@ -336,6 +340,7 @@ var _ = Describe("race", func() { PoolSize: 1000, MinIdleConns: 50, PoolTimeout: 3 * time.Second, + DialTimeout: 1 * time.Second, } p := pool.NewConnPool(opt) diff --git a/options.go b/options.go index c572bbe780..0ebeec342c 100644 --- a/options.go +++ b/options.go @@ -531,6 +531,7 @@ func newConnPool( PoolFIFO: opt.PoolFIFO, PoolSize: opt.PoolSize, PoolTimeout: opt.PoolTimeout, + DialTimeout: opt.DialTimeout, MinIdleConns: opt.MinIdleConns, MaxIdleConns: opt.MaxIdleConns, MaxActiveConns: opt.MaxActiveConns, From 9ef6db16bc78f8a380270d031600cf6fdc2e5d44 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 24 Mar 2025 17:30:29 +0200 Subject: [PATCH 137/230] Drop RedisGears (aka. Triggers and Functions) (#3321) --- .github/workflows/test-redis-enterprise.yml | 1 - commands.go | 1 - gears_commands.go | 149 -------------------- gears_commands_test.go | 121 ---------------- 4 files changed, 272 deletions(-) delete mode 100644 gears_commands.go delete mode 100644 gears_commands_test.go diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 6b533aaa1a..459b2edf00 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -54,5 +54,4 @@ jobs: --ginkgo.skip-file="sentinel_test.go" \ --ginkgo.skip-file="osscluster_test.go" \ --ginkgo.skip-file="pubsub_test.go" \ - --ginkgo.skip-file="gears_commands_test.go" \ --ginkgo.label-filter='!NonRedisEnterprise' diff --git a/commands.go b/commands.go index 034daa2350..d00853901c 100644 --- a/commands.go +++ b/commands.go @@ -211,7 +211,6 @@ type Cmdable interface { ACLCmdable BitMapCmdable ClusterCmdable - GearsCmdable GenericCmdable GeoCmdable HashCmdable diff --git a/gears_commands.go b/gears_commands.go deleted file mode 100644 index e0d49a6b78..0000000000 --- a/gears_commands.go +++ /dev/null @@ -1,149 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "strings" -) - -type GearsCmdable interface { - TFunctionLoad(ctx context.Context, lib string) *StatusCmd - TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd - TFunctionDelete(ctx context.Context, libName string) *StatusCmd - TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd - TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd - TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd - TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd - TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd - TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd -} - -type TFunctionLoadOptions struct { - Replace bool - Config string -} - -type TFunctionListOptions struct { - Withcode bool - Verbose int - Library string -} - -type TFCallOptions struct { - Keys []string - Arguments []string -} - -// TFunctionLoad - load a new JavaScript library into Redis. -// For more information - https://redis.io/commands/tfunction-load/ -func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd { - args := []interface{}{"TFUNCTION", "LOAD", lib} - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd { - args := []interface{}{"TFUNCTION", "LOAD"} - if options != nil { - if options.Replace { - args = append(args, "REPLACE") - } - if options.Config != "" { - args = append(args, "CONFIG", options.Config) - } - } - args = append(args, lib) - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// TFunctionDelete - delete a JavaScript library from Redis. -// For more information - https://redis.io/commands/tfunction-delete/ -func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd { - args := []interface{}{"TFUNCTION", "DELETE", libName} - cmd := NewStatusCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// TFunctionList - list the functions with additional information about each function. -// For more information - https://redis.io/commands/tfunction-list/ -func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd { - args := []interface{}{"TFUNCTION", "LIST"} - cmd := NewMapStringInterfaceSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd { - args := []interface{}{"TFUNCTION", "LIST"} - if options != nil { - if options.Withcode { - args = append(args, "WITHCODE") - } - if options.Verbose != 0 { - v := strings.Repeat("v", options.Verbose) - args = append(args, v) - } - if options.Library != "" { - args = append(args, "LIBRARY", options.Library) - } - } - cmd := NewMapStringInterfaceSliceCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// TFCall - invoke a function. -// For more information - https://redis.io/commands/tfcall/ -func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd { - lf := libName + "." + funcName - args := []interface{}{"TFCALL", lf, numKeys} - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd { - lf := libName + "." + funcName - args := []interface{}{"TFCALL", lf, numKeys} - if options != nil { - for _, key := range options.Keys { - args = append(args, key) - } - for _, key := range options.Arguments { - args = append(args, key) - } - } - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine). -// For more information - https://redis.io/commands/TFCallASYNC/ -func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd { - lf := fmt.Sprintf("%s.%s", libName, funcName) - args := []interface{}{"TFCALLASYNC", lf, numKeys} - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} - -func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd { - lf := fmt.Sprintf("%s.%s", libName, funcName) - args := []interface{}{"TFCALLASYNC", lf, numKeys} - if options != nil { - for _, key := range options.Keys { - args = append(args, key) - } - for _, key := range options.Arguments { - args = append(args, key) - } - } - cmd := NewCmd(ctx, args...) - _ = c(ctx, cmd) - return cmd -} diff --git a/gears_commands_test.go b/gears_commands_test.go deleted file mode 100644 index 7d30995853..0000000000 --- a/gears_commands_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package redis_test - -import ( - "context" - "fmt" - - . "github.com/bsm/ginkgo/v2" - . "github.com/bsm/gomega" - - "github.com/redis/go-redis/v9" -) - -func libCode(libName string) string { - return fmt.Sprintf("#!js api_version=1.0 name=%s\n redis.registerFunction('foo', ()=>{{return 'bar'}})", libName) -} - -func libCodeWithConfig(libName string) string { - lib := `#!js api_version=1.0 name=%s - - var last_update_field_name = "__last_update__" - - if (redis.config.last_update_field_name !== undefined) { - if (typeof redis.config.last_update_field_name != 'string') { - throw "last_update_field_name must be a string"; - } - last_update_field_name = redis.config.last_update_field_name - } - - redis.registerFunction("hset", function(client, key, field, val){ - // get the current time in ms - var curr_time = client.call("time")[0]; - return client.call('hset', key, field, val, last_update_field_name, curr_time); - });` - return fmt.Sprintf(lib, libName) -} - -// TODO: Drop Gears -var _ = Describe("RedisGears commands", Label("gears"), func() { - ctx := context.TODO() - var client *redis.Client - - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - client.TFunctionDelete(ctx, "lib1") - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - opt := &redis.TFunctionLoadOptions{Replace: true, Config: `{"last_update_field_name":"last_update"}`} - resultAdd, err = client.TFunctionLoadArgs(ctx, libCodeWithConfig("lib1"), opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - }) - It("should TFunctionList", Label("gears", "tfunctionlist"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - resultList, err := client.TFunctionList(ctx).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultList[0]["engine"]).To(BeEquivalentTo("js")) - opt := &redis.TFunctionListOptions{Withcode: true, Verbose: 2} - resultListArgs, err := client.TFunctionListArgs(ctx, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultListArgs[0]["code"]).NotTo(BeEquivalentTo("")) - }) - - It("should TFCall", Label("gears", "tfcall"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - var resultAdd interface{} - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - resultAdd, err = client.TFCall(ctx, "lib1", "foo", 0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("bar")) - }) - - It("should TFCallArgs", Label("gears", "tfcallargs"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - var resultAdd interface{} - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}} - resultAdd, err = client.TFCallArgs(ctx, "lib1", "foo", 0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("bar")) - }) - - It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - var resultAdd interface{} - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - resultAdd, err = client.TFCallASYNC(ctx, "lib1", "foo", 0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("bar")) - }) - - It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() { - SkipAfterRedisVersion(7.4, "gears are not working in later versions") - var resultAdd interface{} - resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("OK")) - opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}} - resultAdd, err = client.TFCallASYNCArgs(ctx, "lib1", "foo", 0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo("bar")) - }) -}) From 38ce817c8597d323187a4800f35e80fb57f3ad86 Mon Sep 17 00:00:00 2001 From: Oleglacto Date: Mon, 24 Mar 2025 19:36:21 +0300 Subject: [PATCH 138/230] added `Do` method for raw query by single conn from `pool.Conn()` (#3182) * added `Do` method for raw query by single conn from `pool.Conn()` * added test to cmdble Do method * fixed test * moved Do cmd to commands.go --------- Co-authored-by: Oleg Laktyushkin Co-authored-by: Nedyalko Dyakov --- commands.go | 6 ++++++ commands_test.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/commands.go b/commands.go index d00853901c..6321c15e2d 100644 --- a/commands.go +++ b/commands.go @@ -422,6 +422,12 @@ func (c cmdable) Ping(ctx context.Context) *StatusCmd { return cmd } +func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd { + cmd := NewCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) Quit(_ context.Context) *StatusCmd { panic("not implemented") } diff --git a/commands_test.go b/commands_test.go index a9e90fc993..55b9574964 100644 --- a/commands_test.go +++ b/commands_test.go @@ -84,6 +84,12 @@ var _ = Describe("Commands", func() { Expect(ping.Val()).To(Equal("PONG")) }) + It("should Ping with Do method", func() { + result := client.Conn().Do(ctx, "PING") + Expect(result.Err()).NotTo(HaveOccurred()) + Expect(result.Val()).To(Equal("PONG")) + }) + It("should Wait", func() { const wait = 3 * time.Second From 0a226037ec7113a930ff1a621d7e11436eac1caf Mon Sep 17 00:00:00 2001 From: Nikolay Dubina Date: Tue, 25 Mar 2025 15:28:25 +0800 Subject: [PATCH 139/230] Feature more prominently how to enable OpenTelemetry instrumentation (#3316) --- .github/wordlist.txt | 1 + README.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 1fc34f733c..578616b9d0 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -29,6 +29,7 @@ Lua MSSQL namespace NoSQL +OpenTelemetry ORM Packagist PhpRedis diff --git a/README.md b/README.md index 335d32dad7..fc64e8dd9c 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,24 @@ func ExampleClient() *redis.Client { ``` +### Instrument with OpenTelemetry + +```go +import ( + "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/extra/redisotel/v9" + "errors" +) + +func main() { + ... + rdb := redis.NewClient(&redis.Options{...}) + + if err := errors.Join(redisotel.InstrumentTracing(rdb), redisotel.InstrumentMetrics(rdb)); err != nil { + log.Fatal(err) + } +``` + ### Advanced Configuration From 593d87459115104d5a7191996ef9e72d5034dec1 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 25 Mar 2025 12:25:35 +0200 Subject: [PATCH 140/230] Sync master with v9.8.0-beta.1 (#3322) --- README.md | 21 +++++++++++++++++++-- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 13 files changed, 35 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fc64e8dd9c..6c42c0fc2c 100644 --- a/README.md +++ b/README.md @@ -233,9 +233,26 @@ val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}) In the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries. -## Contributing +**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by passing the desired dialect in the arguments of the command you want to execute. +For example: +``` + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@pickup_zone:[CONTAINS $bike]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "bike": "POINT(-0.1278 51.5074)", + }, + DialectVersion: 3, + }, + ).Result() +``` +You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/). -Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! +## Contributing +We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub. +We appreciate your help in making go-redis better for everyone. +If you are interested in contributing to the go-redis library, please check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to get started. ## Look and feel diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 0699671c59..727fbbd7fd 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.8.0-beta.1 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 72ab52aaf2..775e3e7b19 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.3 +require github.com/redis/go-redis/v9 v9.8.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index f14f54df1f..33d3ef6d80 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.8.0-beta.1 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index e2bc161a4f..363c93c294 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.3 +require github.com/redis/go-redis/v9 v9.8.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 299e4b02c2..5a060d99a7 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/extra/redisotel/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0-beta.1 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 044945654c..3d7a4caaa8 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.3 +require github.com/redis/go-redis/v9 v9.8.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index f44536075a..33d3ef6d80 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.8.0-beta.1 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 11230414ba..7033e805f6 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0-beta.1 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index d64ad57011..c1cff3e90d 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.8.0-beta.1 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 13899eb579..e5b442e614 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0-beta.1 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index fa3c43ae05..8bff000869 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.8.0-beta.1 ) require ( diff --git a/version.go b/version.go index a4832fc1e0..b547951687 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.7.3" + return "9.8.0-beta.1" } From 01b19ff2846bd712119c5bc8a916bf54907f404b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:05:36 +0000 Subject: [PATCH 141/230] DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush (#3234) * DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush * DOC-4464 improved variable names --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- doctests/cmds_list_test.go | 323 +++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 doctests/cmds_list_test.go diff --git a/doctests/cmds_list_test.go b/doctests/cmds_list_test.go new file mode 100644 index 0000000000..ee4a40a0c7 --- /dev/null +++ b/doctests/cmds_list_test.go @@ -0,0 +1,323 @@ +// EXAMPLE: cmds_list +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cmd_llen() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START llen + lPushResult1, err := rdb.LPush(ctx, "mylist", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPushResult1) // >>> 1 + + lPushResult2, err := rdb.LPush(ctx, "mylist", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPushResult2) // >>> 2 + + lLenResult, err := rdb.LLen(ctx, "mylist").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lLenResult) // >>> 2 + // STEP_END + + // Output: + // 1 + // 2 + // 2 +} +func ExampleClient_cmd_lpop() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START lpop + RPushResult, err := rdb.RPush(ctx, + "mylist", "one", "two", "three", "four", "five", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(RPushResult) // >>> 5 + + lPopResult, err := rdb.LPop(ctx, "mylist").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPopResult) // >>> one + + lPopCountResult, err := rdb.LPopCount(ctx, "mylist", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPopCountResult) // >>> [two three] + + lRangeResult, err := rdb.LRange(ctx, "mylist", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult) // >>> [four five] + // STEP_END + + // Output: + // 5 + // one + // [two three] + // [four five] +} + +func ExampleClient_cmd_lpush() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START lpush + lPushResult1, err := rdb.LPush(ctx, "mylist", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPushResult1) // >>> 1 + + lPushResult2, err := rdb.LPush(ctx, "mylist", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(lPushResult2) // >>> 2 + + lRangeResult, err := rdb.LRange(ctx, "mylist", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult) // >>> [Hello World] + // STEP_END + + // Output: + // 1 + // 2 + // [Hello World] +} + +func ExampleClient_cmd_lrange() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START lrange + RPushResult, err := rdb.RPush(ctx, "mylist", + "one", "two", "three", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(RPushResult) // >>> 3 + + lRangeResult1, err := rdb.LRange(ctx, "mylist", 0, 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult1) // >>> [one] + + lRangeResult2, err := rdb.LRange(ctx, "mylist", -3, 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult2) // >>> [one two three] + + lRangeResult3, err := rdb.LRange(ctx, "mylist", -100, 100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult3) // >>> [one two three] + + lRangeResult4, err := rdb.LRange(ctx, "mylist", 5, 10).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult4) // >>> [] + // STEP_END + + // Output: + // 3 + // [one] + // [one two three] + // [one two three] + // [] +} + +func ExampleClient_cmd_rpop() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START rpop + rPushResult, err := rdb.RPush(ctx, "mylist", + "one", "two", "three", "four", "five", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(rPushResult) // >>> 5 + + rPopResult, err := rdb.RPop(ctx, "mylist").Result() + + if err != nil { + panic(err) + } + + fmt.Println(rPopResult) // >>> five + + rPopCountResult, err := rdb.RPopCount(ctx, "mylist", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(rPopCountResult) // >>> [four three] + + lRangeResult, err := rdb.LRange(ctx, "mylist", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult) // >>> [one two] + // STEP_END + + // Output: + // 5 + // five + // [four three] + // [one two] +} + +func ExampleClient_cmd_rpush() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mylist") + // REMOVE_END + + // STEP_START rpush + rPushResult1, err := rdb.RPush(ctx, "mylist", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(rPushResult1) // >>> 1 + + rPushResult2, err := rdb.RPush(ctx, "mylist", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(rPushResult2) // >>> 2 + + lRangeResult, err := rdb.LRange(ctx, "mylist", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(lRangeResult) // >>> [Hello World] + // STEP_END + + // Output: + // 1 + // 2 + // [Hello World] +} From ef7f46a0db90dbbdcf9d2ab02fbaef301ac57ec3 Mon Sep 17 00:00:00 2001 From: Liu Shuang Date: Thu, 3 Apr 2025 21:10:31 +0800 Subject: [PATCH 142/230] update pubsub.go (#3329) --- pubsub.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubsub.go b/pubsub.go index 72b18f49a7..20c085f1f8 100644 --- a/pubsub.go +++ b/pubsub.go @@ -432,7 +432,7 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int return nil, err } - err = cn.WithReader(context.Background(), timeout, func(rd *proto.Reader) error { + err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error { return c.cmd.readReply(rd) }) From 501d5354ccf14022990768375859da5594f4d4e3 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 3 Apr 2025 16:10:51 +0300 Subject: [PATCH 143/230] use 8.0-RC1 (#3330) --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- search_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index def48baf8f..2edb16d398 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M05"]="8.0-M05-pre" + ["8.0-RC1"]="8.0-RC1-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48bbdb7510..f88ca67224 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M05" # 8.0 milestone 5 + - "8.0-RC1" # 8.0 RC1 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M05"]="8.0-M05-pre" + ["8.0-RC1"]="8.0-RC1-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M05" # 8.0 milestone 5 + - "8.0-RC1" # 8.0 RC1 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/search_test.go b/search_test.go index 296f5bd8b4..4359b02fdf 100644 --- a/search_test.go +++ b/search_test.go @@ -381,7 +381,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - SkipBeforeRedisVersion(7.9, "default scorer is not BM25") + SkipBeforeRedisVersion(7.9, "default scorer is not BM25STD") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() From 40e6c906f80b2c298488baf07b8b3de60ac76751 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 3 Apr 2025 17:01:34 +0300 Subject: [PATCH 144/230] drop ft.profile that was never enabled (#3323) --- search_commands.go | 213 --------------------------------------------- search_test.go | 90 ------------------- 2 files changed, 303 deletions(-) diff --git a/search_commands.go b/search_commands.go index 8be39d2a19..85e1256146 100644 --- a/search_commands.go +++ b/search_commands.go @@ -2090,216 +2090,3 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str _ = c(ctx, cmd) return cmd } - -// TODO: remove FTProfile -// type FTProfileResult struct { -// Results []interface{} -// Profile ProfileDetails -// } - -// type ProfileDetails struct { -// TotalProfileTime string -// ParsingTime string -// PipelineCreationTime string -// Warning string -// IteratorsProfile []IteratorProfile -// ResultProcessorsProfile []ResultProcessorProfile -// } - -// type IteratorProfile struct { -// Type string -// QueryType string -// Time interface{} -// Counter int -// Term string -// Size int -// ChildIterators []IteratorProfile -// } - -// type ResultProcessorProfile struct { -// Type string -// Time interface{} -// Counter int -// } - -// func parseFTProfileResult(data []interface{}) (FTProfileResult, error) { -// var result FTProfileResult -// if len(data) < 2 { -// return result, fmt.Errorf("unexpected data length") -// } - -// // Parse results -// result.Results = data[0].([]interface{}) - -// // Parse profile details -// profileData := data[1].([]interface{}) -// profileDetails := ProfileDetails{} -// for i := 0; i < len(profileData); i += 2 { -// switch profileData[i].(string) { -// case "Total profile time": -// profileDetails.TotalProfileTime = profileData[i+1].(string) -// case "Parsing time": -// profileDetails.ParsingTime = profileData[i+1].(string) -// case "Pipeline creation time": -// profileDetails.PipelineCreationTime = profileData[i+1].(string) -// case "Warning": -// profileDetails.Warning = profileData[i+1].(string) -// case "Iterators profile": -// profileDetails.IteratorsProfile = parseIteratorsProfile(profileData[i+1].([]interface{})) -// case "Result processors profile": -// profileDetails.ResultProcessorsProfile = parseResultProcessorsProfile(profileData[i+1].([]interface{})) -// } -// } - -// result.Profile = profileDetails -// return result, nil -// } - -// func parseIteratorsProfile(data []interface{}) []IteratorProfile { -// var iterators []IteratorProfile -// for _, item := range data { -// profile := item.([]interface{}) -// iterator := IteratorProfile{} -// for i := 0; i < len(profile); i += 2 { -// switch profile[i].(string) { -// case "Type": -// iterator.Type = profile[i+1].(string) -// case "Query type": -// iterator.QueryType = profile[i+1].(string) -// case "Time": -// iterator.Time = profile[i+1] -// case "Counter": -// iterator.Counter = int(profile[i+1].(int64)) -// case "Term": -// iterator.Term = profile[i+1].(string) -// case "Size": -// iterator.Size = int(profile[i+1].(int64)) -// case "Child iterators": -// iterator.ChildIterators = parseChildIteratorsProfile(profile[i+1].([]interface{})) -// } -// } -// iterators = append(iterators, iterator) -// } -// return iterators -// } - -// func parseChildIteratorsProfile(data []interface{}) []IteratorProfile { -// var iterators []IteratorProfile -// for _, item := range data { -// profile := item.([]interface{}) -// iterator := IteratorProfile{} -// for i := 0; i < len(profile); i += 2 { -// switch profile[i].(string) { -// case "Type": -// iterator.Type = profile[i+1].(string) -// case "Query type": -// iterator.QueryType = profile[i+1].(string) -// case "Time": -// iterator.Time = profile[i+1] -// case "Counter": -// iterator.Counter = int(profile[i+1].(int64)) -// case "Term": -// iterator.Term = profile[i+1].(string) -// case "Size": -// iterator.Size = int(profile[i+1].(int64)) -// } -// } -// iterators = append(iterators, iterator) -// } -// return iterators -// } - -// func parseResultProcessorsProfile(data []interface{}) []ResultProcessorProfile { -// var processors []ResultProcessorProfile -// for _, item := range data { -// profile := item.([]interface{}) -// processor := ResultProcessorProfile{} -// for i := 0; i < len(profile); i += 2 { -// switch profile[i].(string) { -// case "Type": -// processor.Type = profile[i+1].(string) -// case "Time": -// processor.Time = profile[i+1] -// case "Counter": -// processor.Counter = int(profile[i+1].(int64)) -// } -// } -// processors = append(processors, processor) -// } -// return processors -// } - -// func NewFTProfileCmd(ctx context.Context, args ...interface{}) *FTProfileCmd { -// return &FTProfileCmd{ -// baseCmd: baseCmd{ -// ctx: ctx, -// args: args, -// }, -// } -// } - -// type FTProfileCmd struct { -// baseCmd -// val FTProfileResult -// } - -// func (cmd *FTProfileCmd) String() string { -// return cmdString(cmd, cmd.val) -// } - -// func (cmd *FTProfileCmd) SetVal(val FTProfileResult) { -// cmd.val = val -// } - -// func (cmd *FTProfileCmd) Result() (FTProfileResult, error) { -// return cmd.val, cmd.err -// } - -// func (cmd *FTProfileCmd) Val() FTProfileResult { -// return cmd.val -// } - -// func (cmd *FTProfileCmd) readReply(rd *proto.Reader) (err error) { -// data, err := rd.ReadSlice() -// if err != nil { -// return err -// } -// cmd.val, err = parseFTProfileResult(data) -// if err != nil { -// cmd.err = err -// } -// return nil -// } - -// // FTProfile - Executes a search query and returns a profile of how the query was processed. -// // The 'index' parameter specifies the index to search, the 'limited' parameter specifies whether to limit the results, -// // and the 'query' parameter specifies the search / aggreagte query. Please notice that you must either pass a SearchQuery or an AggregateQuery. -// // For more information, please refer to the Redis documentation: -// // [FT.PROFILE]: (https://redis.io/commands/ft.profile/) -// func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, query interface{}) *FTProfileCmd { -// queryType := "" -// var argsQuery []interface{} - -// switch v := query.(type) { -// case AggregateQuery: -// queryType = "AGGREGATE" -// argsQuery = v -// case SearchQuery: -// queryType = "SEARCH" -// argsQuery = v -// default: -// panic("FT.PROFILE: query must be either AggregateQuery or SearchQuery") -// } - -// args := []interface{}{"FT.PROFILE", index, queryType} - -// if limited { -// args = append(args, "LIMITED") -// } -// args = append(args, "QUERY") -// args = append(args, argsQuery...) - -// cmd := NewFTProfileCmd(ctx, args...) -// _ = c(ctx, cmd) -// return cmd -// } diff --git a/search_test.go b/search_test.go index 4359b02fdf..4d8417d783 100644 --- a/search_test.go +++ b/search_test.go @@ -1694,96 +1694,6 @@ func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []str Expect(result.Total).To(BeEquivalentTo(len(expectedDocIDs))) } -// It("should FTProfile Search and Aggregate", Label("search", "ftprofile"), func() { -// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() -// Expect(err).NotTo(HaveOccurred()) -// Expect(val).To(BeEquivalentTo("OK")) -// WaitForIndexing(client, "idx1") - -// client.HSet(ctx, "1", "t", "hello") -// client.HSet(ctx, "2", "t", "world") - -// // FTProfile Search -// query := redis.FTSearchQuery("hello|world", &redis.FTSearchOptions{NoContent: true}) -// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() -// Expect(err).NotTo(HaveOccurred()) -// panic(res1) -// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) -// resProfile := res1["profile"].(map[interface{}]interface{}) -// Expect(resProfile["Parsing time"].(float64) < 0.5).To(BeTrue()) -// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) -// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2.0)) -// Expect(iterProfile0["Type"]).To(BeEquivalentTo("UNION")) - -// // FTProfile Aggregate -// aggQuery := redis.FTAggregateQuery("*", &redis.FTAggregateOptions{ -// Load: []redis.FTAggregateLoad{{Field: "t"}}, -// Apply: []redis.FTAggregateApply{{Field: "startswith(@t, 'hel')", As: "prefix"}}}) -// res2, err := client.FTProfile(ctx, "idx1", false, aggQuery).Result() -// Expect(err).NotTo(HaveOccurred()) -// Expect(len(res2["results"].([]interface{}))).To(BeEquivalentTo(2)) -// resProfile = res2["profile"].(map[interface{}]interface{}) -// iterProfile0 = resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) -// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) -// Expect(iterProfile0["Type"]).To(BeEquivalentTo("WILDCARD")) -// }) - -// It("should FTProfile Search Limited", Label("search", "ftprofile"), func() { -// val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}).Result() -// Expect(err).NotTo(HaveOccurred()) -// Expect(val).To(BeEquivalentTo("OK")) -// WaitForIndexing(client, "idx1") - -// client.HSet(ctx, "1", "t", "hello") -// client.HSet(ctx, "2", "t", "hell") -// client.HSet(ctx, "3", "t", "help") -// client.HSet(ctx, "4", "t", "helowa") - -// // FTProfile Search -// query := redis.FTSearchQuery("%hell% hel*", &redis.FTSearchOptions{}) -// res1, err := client.FTProfile(ctx, "idx1", true, query).Result() -// Expect(err).NotTo(HaveOccurred()) -// resProfile := res1["profile"].(map[interface{}]interface{}) -// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) -// Expect(iterProfile0["Type"]).To(BeEquivalentTo("INTERSECT")) -// Expect(len(res1["results"].([]interface{}))).To(BeEquivalentTo(3)) -// Expect(iterProfile0["Child iterators"].([]interface{})[0].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 3")) -// Expect(iterProfile0["Child iterators"].([]interface{})[1].(map[interface{}]interface{})["Child iterators"]).To(BeEquivalentTo("The number of iterators in the union is 4")) -// }) - -// It("should FTProfile Search query params", Label("search", "ftprofile"), func() { -// hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} -// val, err := client.FTCreate(ctx, "idx1", -// &redis.FTCreateOptions{}, -// &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() -// Expect(err).NotTo(HaveOccurred()) -// Expect(val).To(BeEquivalentTo("OK")) -// WaitForIndexing(client, "idx1") - -// client.HSet(ctx, "a", "v", "aaaaaaaa") -// client.HSet(ctx, "b", "v", "aaaabaaa") -// client.HSet(ctx, "c", "v", "aaaaabaa") - -// // FTProfile Search -// searchOptions := &redis.FTSearchOptions{ -// Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, -// SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, -// DialectVersion: 2, -// Params: map[string]interface{}{"vec": "aaaaaaaa"}, -// } -// query := redis.FTSearchQuery("*=>[KNN 2 @v $vec]", searchOptions) -// res1, err := client.FTProfile(ctx, "idx1", false, query).Result() -// Expect(err).NotTo(HaveOccurred()) -// resProfile := res1["profile"].(map[interface{}]interface{}) -// iterProfile0 := resProfile["Iterators profile"].([]interface{})[0].(map[interface{}]interface{}) -// Expect(iterProfile0["Counter"]).To(BeEquivalentTo(2)) -// Expect(iterProfile0["Type"]).To(BeEquivalentTo(redis.SearchFieldTypeVector.String())) -// Expect(res1["total_results"]).To(BeEquivalentTo(2)) -// results0 := res1["results"].([]interface{})[0].(map[interface{}]interface{}) -// Expect(results0["id"]).To(BeEquivalentTo("a")) -// Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) -// }) - var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "NonRedisEnterprise"), func() { var clientResp2 *redis.Client From 4e6dd15bd616204dbd6b7de1aac04d07698cad1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:21:55 +0300 Subject: [PATCH 145/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3336) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.47.0 to 0.48.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.47.0...0.48.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index beefa6164f..4d0fc338d6 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.47.0 + uses: rojopolis/spellcheck-github-actions@0.48.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From ab8deac84fd76277ceac8f86ac54dc860a4c6655 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:39:59 +0300 Subject: [PATCH 146/230] Fix FT.Search Limit argument and add CountOnly argument for limit 0 0 (#3338) * Fix Limit argument and add CountOnly argument * Add test and Documentation * Update search_commands.go --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_commands.go | 15 +++++++++++---- search_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/search_commands.go b/search_commands.go index 85e1256146..4094262095 100644 --- a/search_commands.go +++ b/search_commands.go @@ -320,8 +320,11 @@ type FTSearchOptions struct { SortByWithCount bool LimitOffset int Limit int - Params map[string]interface{} - DialectVersion int + // CountOnly sets LIMIT 0 0 to get the count - number of documents in the result set without actually returning the result set. + // When using this option, the Limit and LimitOffset options are ignored. + CountOnly bool + Params map[string]interface{} + DialectVersion int } type FTSynDumpResult struct { @@ -1954,8 +1957,12 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin args = append(args, "WITHCOUNT") } } - if options.LimitOffset >= 0 && options.Limit > 0 { - args = append(args, "LIMIT", options.LimitOffset, options.Limit) + if options.CountOnly { + args = append(args, "LIMIT", 0, 0) + } else { + if options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) + } } if options.Params != nil { args = append(args, "PARAMS", len(options.Params)*2) diff --git a/search_test.go b/search_test.go index 4d8417d783..3c4457a45d 100644 --- a/search_test.go +++ b/search_test.go @@ -1683,6 +1683,44 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) }) + It("should test ft.search with CountOnly param", Label("search", "ftsearch"), func() { + val, err := client.FTCreate(ctx, "txtIndex", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txtIndex") + + _, err = client.HSet(ctx, "doc1", "txt", "hello world").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.HSet(ctx, "doc2", "txt", "hello go").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.HSet(ctx, "doc3", "txt", "hello redis").Result() + Expect(err).NotTo(HaveOccurred()) + + optsCountOnly := &redis.FTSearchOptions{ + CountOnly: true, + LimitOffset: 0, + Limit: 2, // even though we limit to 2, with count-only no docs are returned + DialectVersion: 2, + } + resCountOnly, err := client.FTSearchWithArgs(ctx, "txtIndex", "hello", optsCountOnly).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resCountOnly.Total).To(BeEquivalentTo(3)) + Expect(len(resCountOnly.Docs)).To(BeEquivalentTo(0)) + + optsLimit := &redis.FTSearchOptions{ + CountOnly: false, + LimitOffset: 0, + Limit: 2, // we expect to get 2 documents even though total count is 3 + DialectVersion: 2, + } + resLimit, err := client.FTSearchWithArgs(ctx, "txtIndex", "hello", optsLimit).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resLimit.Total).To(BeEquivalentTo(3)) + Expect(len(resLimit.Docs)).To(BeEquivalentTo(2)) + }) + }) func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { From 41b4985a57514d6c1b700d7c0efb23376e1737dc Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:19:53 +0300 Subject: [PATCH 147/230] fix add missing command in interface (#3344) --- hash_commands.go | 1 + 1 file changed, 1 insertion(+) diff --git a/hash_commands.go b/hash_commands.go index 1f53f344d9..50d94bae73 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -13,6 +13,7 @@ type HashCmdable interface { HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd + HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd HKeys(ctx context.Context, key string) *StringSliceCmd HLen(ctx context.Context, key string) *IntCmd From e2d1cd584dae65b89c3ce4b0899a45a880a27212 Mon Sep 17 00:00:00 2001 From: Bulat Khasanov Date: Tue, 15 Apr 2025 16:57:50 +0300 Subject: [PATCH 148/230] Use DB option in NewFailoverClusterClient (#3342) --- sentinel.go | 16 ++++++++++++++++ sentinel_test.go | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/sentinel.go b/sentinel.go index a4c9f53c40..a132af2fe5 100644 --- a/sentinel.go +++ b/sentinel.go @@ -815,6 +815,22 @@ func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient { } opt := failoverOpt.clusterOptions() + if failoverOpt.DB != 0 { + onConnect := opt.OnConnect + + opt.OnConnect = func(ctx context.Context, cn *Conn) error { + if err := cn.Select(ctx, failoverOpt.DB).Err(); err != nil { + return err + } + + if onConnect != nil { + return onConnect(ctx, cn) + } + + return nil + } + } + opt.ClusterSlots = func(ctx context.Context) ([]ClusterSlot, error) { masterAddr, err := failover.MasterAddr(ctx) if err != nil { diff --git a/sentinel_test.go b/sentinel_test.go index b34706f89a..07c7628a06 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -200,6 +200,7 @@ var _ = Describe("NewFailoverClusterClient", func() { SentinelAddrs: sentinelAddrs, RouteRandomly: true, + DB: 1, }) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) @@ -289,6 +290,20 @@ var _ = Describe("NewFailoverClusterClient", func() { }) }) + It("should sentinel cluster client db", func() { + err := client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error { + return c.Ping(ctx).Err() + }) + Expect(err).NotTo(HaveOccurred()) + + _ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error { + clientInfo, err := c.ClientInfo(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(clientInfo.DB).To(Equal(1)) + return nil + }) + }) + It("should sentinel cluster PROTO 3", func() { _ = client.ForEachShard(ctx, func(ctx context.Context, c *redis.Client) error { val, err := client.Do(ctx, "HELLO").Result() From 7f9db6d9baa822e0ba172d92c3c240f071f94214 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:55:44 +0100 Subject: [PATCH 149/230] DOC-5102 added CountOnly search example for docs (#3345) --- doctests/home_json_example_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index ec2843ad35..4ee93d79d6 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -152,6 +152,32 @@ func ExampleClient_search_json() { // >>> Tel Aviv // STEP_END + // STEP_START query2count_only + citiesResult2, err := rdb.FTSearchWithArgs( + ctx, + "idx:users", + "Paul", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "$.city", + As: "city", + }, + }, + CountOnly: true, + }, + ).Result() + + if err != nil { + panic(err) + } + + // The `Total` field has the correct number of docs found + // by the query but the `Docs` slice is empty. + fmt.Println(len(citiesResult2.Docs)) // >>> 0 + fmt.Println(citiesResult2.Total) // >>> 2 + // STEP_END + // STEP_START query3 aggOptions := redis.FTAggregateOptions{ GroupBy: []redis.FTAggregateGroupBy{ @@ -196,6 +222,8 @@ func ExampleClient_search_json() { // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} // London // Tel Aviv + // 0 + // 2 // London - 1 // Tel Aviv - 2 } From e9570a39693b947e7b985f92e419a1600063dda1 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:59:20 +0300 Subject: [PATCH 150/230] Add integration tests for Redis 8 behavior changes in Redis Search (#3337) * Add integration tests for Redis 8 behavior changes in Redis Search * Undo changes in ft.search limit * Fix BM25 as the default scorer test * Add more tests and comments on deprecated params * Update search_commands.go * Remove deprication comment for nostopwords --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_commands.go | 12 +- search_test.go | 477 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 485 insertions(+), 4 deletions(-) diff --git a/search_commands.go b/search_commands.go index 4094262095..b31baaa760 100644 --- a/search_commands.go +++ b/search_commands.go @@ -114,6 +114,7 @@ type SpellCheckTerms struct { } type FTExplainOptions struct { + // Dialect 1,3 and 4 are deprecated since redis 8.0 Dialect string } @@ -261,7 +262,8 @@ type FTAggregateOptions struct { WithCursor bool WithCursorOptions *FTAggregateWithCursor Params map[string]interface{} - DialectVersion int + // Dialect 1,3 and 4 are deprecated since redis 8.0 + DialectVersion int } type FTSearchFilter struct { @@ -322,8 +324,9 @@ type FTSearchOptions struct { Limit int // CountOnly sets LIMIT 0 0 to get the count - number of documents in the result set without actually returning the result set. // When using this option, the Limit and LimitOffset options are ignored. - CountOnly bool - Params map[string]interface{} + CountOnly bool + Params map[string]interface{} + // Dialect 1,3 and 4 are deprecated since redis 8.0 DialectVersion int } @@ -440,7 +443,8 @@ type IndexDefinition struct { type FTSpellCheckOptions struct { Distance int Terms *FTSpellCheckTerms - Dialect int + // Dialect 1,3 and 4 are deprecated since redis 8.0 + Dialect int } type FTSpellCheckTerms struct { diff --git a/search_test.go b/search_test.go index 3c4457a45d..6bc8b11123 100644 --- a/search_test.go +++ b/search_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "strings" "time" . "github.com/bsm/ginkgo/v2" @@ -1683,6 +1684,389 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) }) + It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{ + FieldName: "txt", + FieldType: redis.SearchFieldTypeText, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "testIdx") + + client.HSet(ctx, "doc1", "txt", "hello world") + + // Attempt to search with a non-zero offset and zero limit. + _, err = client.FTSearchWithArgs(ctx, "testIdx", "hello", &redis.FTSearchOptions{ + LimitOffset: 5, + Limit: 0, + }).Result() + Expect(err).To(HaveOccurred()) + }) + + It("should evaluate exponentiation precedence in APPLY expressions correctly", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "txns", &redis.FTCreateOptions{}, &redis.FieldSchema{ + FieldName: "dummy", + FieldType: redis.SearchFieldTypeText, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txns") + + client.HSet(ctx, "doc1", "dummy", "dummy") + + correctOptions := &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{ + {Field: "(2*3^2)", As: "Value"}, + }, + Limit: 1, + LimitOffset: 0, + } + correctRes, err := client.FTAggregateWithArgs(ctx, "txns", "*", correctOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(correctRes.Rows[0].Fields["Value"]).To(BeEquivalentTo("18")) + }) + + It("should return a syntax error when empty strings are used for numeric parameters", Label("search", "ftsearch"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "idx", &redis.FTCreateOptions{}, &redis.FieldSchema{ + FieldName: "n", + FieldType: redis.SearchFieldTypeNumeric, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx") + + client.HSet(ctx, "doc1", "n", 0) + + _, err = client.FTSearchWithArgs(ctx, "idx", "*", &redis.FTSearchOptions{ + Filters: []redis.FTSearchFilter{{ + FieldName: "n", + Min: "", + Max: "", + }}, + DialectVersion: 2, + }).Result() + Expect(err).To(HaveOccurred()) + }) + + It("should return NaN as default for AVG reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestAvg", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestAvg") + + client.HSet(ctx, "doc1", "grp", "g1") + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchAvg, Args: []interface{}{"@n"}, As: "avg"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestAvg", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + + Expect(res.Rows[0].Fields["avg"]).To(SatisfyAny(Equal("nan"), Equal("NaN"))) + }) + + It("should return 1 as default for COUNT reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestCount", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestCount") + + client.HSet(ctx, "doc1", "grp", "g1") + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchCount, As: "cnt"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestCount", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + + Expect(res.Rows[0].Fields["cnt"]).To(BeEquivalentTo("1")) + }) + + It("should return NaN as default for SUM reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestSum", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestSum") + + client.HSet(ctx, "doc1", "grp", "g1") + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchSum, Args: []interface{}{"@n"}, As: "sum"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestSum", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + + Expect(res.Rows[0].Fields["sum"]).To(SatisfyAny(Equal("nan"), Equal("NaN"))) + }) + + It("should return the full requested number of results by re-running the query when some results expire", Label("search", "ftsearch"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggExpired", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "order", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggExpired") + + for i := 1; i <= 15; i++ { + key := fmt.Sprintf("doc%d", i) + _, err := client.HSet(ctx, key, "order", i).Result() + Expect(err).NotTo(HaveOccurred()) + } + + _, err = client.Del(ctx, "doc3", "doc7").Result() + Expect(err).NotTo(HaveOccurred()) + + options := &redis.FTSearchOptions{ + SortBy: []redis.FTSearchSortBy{{FieldName: "order", Asc: true}}, + LimitOffset: 0, + Limit: 10, + } + res, err := client.FTSearchWithArgs(ctx, "aggExpired", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + + Expect(len(res.Docs)).To(BeEquivalentTo(10)) + + for _, doc := range res.Docs { + Expect(doc.ID).ToNot(Or(Equal("doc3"), Equal("doc7"))) + } + }) + + It("should stop processing and return an error when a timeout occurs", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTimeoutHeavy", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTimeoutHeavy") + + const totalDocs = 10000 + for i := 0; i < totalDocs; i++ { + key := fmt.Sprintf("doc%d", i) + _, err := client.HSet(ctx, key, "n", i).Result() + Expect(err).NotTo(HaveOccurred()) + } + + options := &redis.FTAggregateOptions{ + SortBy: []redis.FTAggregateSortBy{{FieldName: "@n", Desc: true}}, + LimitOffset: 0, + Limit: 100, + Timeout: 1, // 1 ms timeout, expected to trigger a timeout error. + } + _, err = client.FTAggregateWithArgs(ctx, "aggTimeoutHeavy", "*", options).Result() + Expect(err).To(HaveOccurred()) + Expect(strings.ToLower(err.Error())).To(ContainSubstring("timeout")) + }) + + It("should return 0 as default for COUNT_DISTINCT reducer when no values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestCountDistinct", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestCountDistinct") + + client.HSet(ctx, "doc1", "grp", "g1") + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchCountDistinct, Args: []interface{}{"@x"}, As: "distinct_count"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + + res, err := client.FTAggregateWithArgs(ctx, "aggTestCountDistinct", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + Expect(res.Rows[0].Fields["distinct_count"]).To(BeEquivalentTo("0")) + }) + + It("should return 0 as default for COUNT_DISTINCTISH reducer when no values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestCountDistinctIsh", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "y", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestCountDistinctIsh") + + _, err = client.HSet(ctx, "doc1", "grp", "g1").Result() + Expect(err).NotTo(HaveOccurred()) + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchCountDistinctish, Args: []interface{}{"@y"}, As: "distinctish_count"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestCountDistinctIsh", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + Expect(res.Rows[0].Fields["distinctish_count"]).To(BeEquivalentTo("0")) + }) + + It("should use BM25 as the default scorer", Label("search", "ftsearch"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "scoringTest", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "scoringTest") + + _, err = client.HSet(ctx, "doc1", "description", "red apple").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.HSet(ctx, "doc2", "description", "green apple").Result() + Expect(err).NotTo(HaveOccurred()) + + resDefault, err := client.FTSearchWithArgs(ctx, "scoringTest", "apple", &redis.FTSearchOptions{WithScores: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDefault.Total).To(BeNumerically(">", 0)) + + resBM25, err := client.FTSearchWithArgs(ctx, "scoringTest", "apple", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resBM25.Total).To(BeNumerically(">", 0)) + Expect(resDefault.Total).To(BeEquivalentTo(resBM25.Total)) + Expect(resDefault.Docs[0].ID).To(BeElementOf("doc1", "doc2")) + Expect(resDefault.Docs[1].ID).To(BeElementOf("doc1", "doc2")) + }) + + It("should return 0 as default for STDDEV reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestStddev", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestStddev") + + _, err = client.HSet(ctx, "doc1", "grp", "g1").Result() + Expect(err).NotTo(HaveOccurred()) + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchStdDev, Args: []interface{}{"@n"}, As: "stddev"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestStddev", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + + Expect(res.Rows[0].Fields["stddev"]).To(BeEquivalentTo("0")) + }) + + It("should return NaN as default for QUANTILE reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestQuantile", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestQuantile") + + _, err = client.HSet(ctx, "doc1", "grp", "g1").Result() + Expect(err).NotTo(HaveOccurred()) + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchQuantile, Args: []interface{}{"@n", 0.5}, As: "quantile"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestQuantile", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + Expect(res.Rows[0].Fields["quantile"]).To(SatisfyAny(Equal("nan"), Equal("NaN"))) + }) + + It("should return nil as default for FIRST_VALUE reducer when no values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestFirstValue", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestFirstValue") + + _, err = client.HSet(ctx, "doc1", "grp", "g1").Result() + Expect(err).NotTo(HaveOccurred()) + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchFirstValue, Args: []interface{}{"@t"}, As: "first_val"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestFirstValue", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + Expect(res.Rows[0].Fields["first_val"]).To(BeNil()) + }) + + It("should fail to add an alias that is an existing index name", Label("search", "ftalias"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + val, err = client.FTCreate(ctx, "idx2", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx2") + + _, err = client.FTAliasAdd(ctx, "idx2", "idx1").Result() + Expect(err).To(HaveOccurred()) + Expect(strings.ToLower(err.Error())).To(ContainSubstring("alias")) + }) + It("should test ft.search with CountOnly param", Label("search", "ftsearch"), func() { val, err := client.FTCreate(ctx, "txtIndex", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}, @@ -1721,6 +2105,99 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(len(resLimit.Docs)).To(BeEquivalentTo(2)) }) + It("should reject deprecated configuration keys", Label("search", "ftconfig"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + // List of deprecated configuration keys. + deprecatedKeys := []string{ + "_FREE_RESOURCE_ON_THREAD", + "_NUMERIC_COMPRESS", + "_NUMERIC_RANGES_PARENTS", + "_PRINT_PROFILE_CLOCK", + "_PRIORITIZE_INTERSECT_UNION_CHILDREN", + "BG_INDEX_SLEEP_GAP", + "CONN_PER_SHARD", + "CURSOR_MAX_IDLE", + "CURSOR_REPLY_THRESHOLD", + "DEFAULT_DIALECT", + "EXTLOAD", + "FORK_GC_CLEAN_THRESHOLD", + "FORK_GC_RETRY_INTERVAL", + "FORK_GC_RUN_INTERVAL", + "FORKGC_SLEEP_BEFORE_EXIT", + "FRISOINI", + "GC_POLICY", + "GCSCANSIZE", + "INDEX_CURSOR_LIMIT", + "MAXAGGREGATERESULTS", + "MAXDOCTABLESIZE", + "MAXPREFIXEXPANSIONS", + "MAXSEARCHRESULTS", + "MIN_OPERATION_WORKERS", + "MIN_PHONETIC_TERM_LEN", + "MINPREFIX", + "MINSTEMLEN", + "NO_MEM_POOLS", + "NOGC", + "ON_TIMEOUT", + "MULTI_TEXT_SLOP", + "PARTIAL_INDEXED_DOCS", + "RAW_DOCID_ENCODING", + "SEARCH_THREADS", + "TIERED_HNSW_BUFFER_LIMIT", + "TIMEOUT", + "TOPOLOGY_VALIDATION_TIMEOUT", + "UNION_ITERATOR_HEAP", + "VSS_MAX_RESIZE", + "WORKERS", + "WORKERS_PRIORITY_BIAS_THRESHOLD", + "MT_MODE", + "WORKER_THREADS", + } + + for _, key := range deprecatedKeys { + _, err := client.FTConfigSet(ctx, key, "test_value").Result() + Expect(err).To(HaveOccurred()) + } + + val, err := client.ConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + // Since FT.CONFIG is deprecated since redis 8, use CONFIG instead with new search parameters. + keys := make([]string, 0, len(val)) + for key := range val { + keys = append(keys, key) + } + Expect(keys).To(ContainElement(ContainSubstring("search"))) + }) + + It("should return INF for MIN reducer and -INF for MAX reducer when no numeric values are present", Label("search", "ftaggregate"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + val, err := client.FTCreate(ctx, "aggTestMinMax", &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "grp", FieldType: redis.SearchFieldTypeText}, + &redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "aggTestMinMax") + + _, err = client.HSet(ctx, "doc1", "grp", "g1").Result() + Expect(err).NotTo(HaveOccurred()) + + reducers := []redis.FTAggregateReducer{ + {Reducer: redis.SearchMin, Args: []interface{}{"@n"}, As: "minValue"}, + {Reducer: redis.SearchMax, Args: []interface{}{"@n"}, As: "maxValue"}, + } + groupBy := []redis.FTAggregateGroupBy{ + {Fields: []interface{}{"@grp"}, Reduce: reducers}, + } + options := &redis.FTAggregateOptions{GroupBy: groupBy} + res, err := client.FTAggregateWithArgs(ctx, "aggTestMinMax", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows).ToNot(BeEmpty()) + + Expect(res.Rows[0].Fields["minValue"]).To(BeEquivalentTo("inf")) + Expect(res.Rows[0].Fields["maxValue"]).To(BeEquivalentTo("-inf")) + }) + }) func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { From 1284ebd6d1b9e058f80c1a8422349249d88ffdac Mon Sep 17 00:00:00 2001 From: Bulat Khasanov Date: Wed, 16 Apr 2025 18:32:57 +0300 Subject: [PATCH 151/230] Use correct slot for COUNTKEYSINSLOT command (#3327) --- internal_test.go | 24 ++++++++++++++++++++++++ osscluster.go | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal_test.go b/internal_test.go index a6317196a6..516ada8236 100644 --- a/internal_test.go +++ b/internal_test.go @@ -352,3 +352,27 @@ var _ = Describe("withConn", func() { Expect(client.connPool.Len()).To(Equal(1)) }) }) + +var _ = Describe("ClusterClient", func() { + var client *ClusterClient + + BeforeEach(func() { + client = &ClusterClient{} + }) + + Describe("cmdSlot", func() { + It("select slot from args for GETKEYSINSLOT command", func() { + cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200) + + slot := client.cmdSlot(context.Background(), cmd) + Expect(slot).To(Equal(100)) + }) + + It("select slot from args for COUNTKEYSINSLOT command", func() { + cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100) + + slot := client.cmdSlot(context.Background(), cmd) + Expect(slot).To(Equal(100)) + }) + }) +}) diff --git a/osscluster.go b/osscluster.go index b018cc9e46..20180464e2 100644 --- a/osscluster.go +++ b/osscluster.go @@ -1856,7 +1856,7 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { func (c *ClusterClient) cmdSlot(ctx context.Context, cmd Cmder) int { args := cmd.Args() - if args[0] == "cluster" && args[1] == "getkeysinslot" { + if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { return args[2].(int) } From 053f2b270c3f38adb13e1f5fc80e8f673052793f Mon Sep 17 00:00:00 2001 From: Naveen Prashanth <78990165+gnpaone@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:02:40 +0530 Subject: [PATCH 152/230] Ensure context isn't exhausted via concurrent query as opposed to sentinel query (#3334) --- sentinel.go | 67 +++++++++++++++++++++++++++++++++++------------- sentinel_test.go | 19 ++++++++++++++ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/sentinel.go b/sentinel.go index a132af2fe5..0638663536 100644 --- a/sentinel.go +++ b/sentinel.go @@ -566,29 +566,60 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { } } - for i, sentinelAddr := range c.sentinelAddrs { - sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr)) + var ( + masterAddr string + wg sync.WaitGroup + once sync.Once + errCh = make(chan error, len(c.sentinelAddrs)) + ) - masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result() - if err != nil { - _ = sentinel.Close() - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return "", err - } - internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s", - c.opt.MasterName, err) - continue - } + ctx, cancel := context.WithCancel(ctx) + defer cancel() - // Push working sentinel to the top. - c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0] - c.setSentinel(ctx, sentinel) + for i, sentinelAddr := range c.sentinelAddrs { + wg.Add(1) + go func(i int, addr string) { + defer wg.Done() + sentinelCli := NewSentinelClient(c.opt.sentinelOptions(addr)) + addrVal, err := sentinelCli.GetMasterAddrByName(ctx, c.opt.MasterName).Result() + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + // Report immediately and return + errCh <- err + return + } + internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName addr=%s, master=%q failed: %s", + addr, c.opt.MasterName, err) + _ = sentinelCli.Close() + return + } - addr := net.JoinHostPort(masterAddr[0], masterAddr[1]) - return addr, nil + once.Do(func() { + masterAddr = net.JoinHostPort(addrVal[0], addrVal[1]) + // Push working sentinel to the top + c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0] + c.setSentinel(ctx, sentinelCli) + internal.Logger.Printf(ctx, "sentinel: selected addr=%s masterAddr=%s", addr, masterAddr) + cancel() + }) + }(i, sentinelAddr) } - return "", errors.New("redis: all sentinels specified in configuration are unreachable") + done := make(chan struct{}) + go func() { + wg.Wait() + close(done) + }() + + select { + case <-done: + if masterAddr != "" { + return masterAddr, nil + } + return "", errors.New("redis: all sentinels specified in configuration are unreachable") + case err := <-errCh: + return "", err + } } func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) { diff --git a/sentinel_test.go b/sentinel_test.go index 07c7628a06..cde7f956d1 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -3,6 +3,7 @@ package redis_test import ( "context" "net" + "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" @@ -32,6 +33,24 @@ var _ = Describe("Sentinel PROTO 2", func() { }) }) +var _ = Describe("Sentinel resolution", func() { + It("should resolve master without context exhaustion", func() { + shortCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) + defer cancel() + + client := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: sentinelName, + SentinelAddrs: sentinelAddrs, + MaxRetries: -1, + }) + + err := client.Ping(shortCtx).Err() + Expect(err).NotTo(HaveOccurred(), "expected master to resolve without context exhaustion") + + _ = client.Close() + }) +}) + var _ = Describe("Sentinel", func() { var client *redis.Client var master *redis.Client From dbe3f50e65faf1a97d03a9739bfbe5758ba99cc3 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:31:07 +0300 Subject: [PATCH 153/230] fix: better error handling when fetching the master node from the sentinels (#3349) * Better error handling when fetching the master node from the sentinels * fix error message generation * close the errCh to not block * use len over errCh --- sentinel.go | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/sentinel.go b/sentinel.go index 0638663536..f5b9a52d10 100644 --- a/sentinel.go +++ b/sentinel.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "net" "strings" "sync" @@ -583,17 +584,12 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { sentinelCli := NewSentinelClient(c.opt.sentinelOptions(addr)) addrVal, err := sentinelCli.GetMasterAddrByName(ctx, c.opt.MasterName).Result() if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - // Report immediately and return - errCh <- err - return - } internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName addr=%s, master=%q failed: %s", addr, c.opt.MasterName, err) _ = sentinelCli.Close() + errCh <- err return } - once.Do(func() { masterAddr = net.JoinHostPort(addrVal[0], addrVal[1]) // Push working sentinel to the top @@ -605,21 +601,16 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { }(i, sentinelAddr) } - done := make(chan struct{}) - go func() { - wg.Wait() - close(done) - }() - - select { - case <-done: - if masterAddr != "" { - return masterAddr, nil - } - return "", errors.New("redis: all sentinels specified in configuration are unreachable") - case err := <-errCh: - return "", err + wg.Wait() + close(errCh) + if masterAddr != "" { + return masterAddr, nil + } + errs := make([]error, 0, len(errCh)) + for err := range errCh { + errs = append(errs, err) } + return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %w", errors.Join(errs...)) } func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) { From e2b230caeb98a5a19827b2b4c8fc9e59a70e6bc6 Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 21 Apr 2025 22:11:00 -0700 Subject: [PATCH 154/230] docs: fix documentation comments (#3351) --- command.go | 3 ++- commands.go | 2 +- hash_commands.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 696501453a..364706e3e7 100644 --- a/command.go +++ b/command.go @@ -3831,7 +3831,8 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error { } // ----------------------------------------------------------------------- -// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}. + +// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}. type MapMapStringInterfaceCmd struct { baseCmd val map[string]interface{} diff --git a/commands.go b/commands.go index 6321c15e2d..123005bc79 100644 --- a/commands.go +++ b/commands.go @@ -330,7 +330,7 @@ func (info LibraryInfo) Validate() error { return nil } -// Hello Set the resp protocol used. +// Hello sets the resp protocol used. func (c statefulCmdable) Hello(ctx context.Context, ver int, username, password, clientName string, ) *MapStringInterfaceCmd { diff --git a/hash_commands.go b/hash_commands.go index 50d94bae73..be58b8d2d0 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -480,7 +480,7 @@ func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *Stri return cmd } -// ExpirationType represents an expiration option for the HGETEX command. +// HGetEXExpirationType represents an expiration option for the HGETEX command. type HGetEXExpirationType string const ( From 2872e0d170fba1d42b454d8a85dd011cf4fbacdc Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:24:57 +0100 Subject: [PATCH 155/230] DOC-5111 added hash search examples (#3357) --- doctests/home_json_example_test.go | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index 4ee93d79d6..f32bf8d10f 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -227,3 +227,107 @@ func ExampleClient_search_json() { // London - 1 // Tel Aviv - 2 } + +func ExampleClient_search_hash() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // REMOVE_START + rdb.Del(ctx, "huser:1", "huser:2", "huser:3") + rdb.FTDropIndex(ctx, "hash-idx:users") + // REMOVE_END + + // STEP_START make_hash_index + _, err := rdb.FTCreate( + ctx, + "hash-idx:users", + // Options: + &redis.FTCreateOptions{ + OnHash: true, + Prefix: []interface{}{"huser:"}, + }, + // Index schema fields: + &redis.FieldSchema{ + FieldName: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "city", + FieldType: redis.SearchFieldTypeTag, + }, + &redis.FieldSchema{ + FieldName: "age", + FieldType: redis.SearchFieldTypeNumeric, + }, + ).Result() + + if err != nil { + panic(err) + } + // STEP_END + + user1 := map[string]interface{}{ + "name": "Paul John", + "email": "paul.john@example.com", + "age": 42, + "city": "London", + } + + user2 := map[string]interface{}{ + "name": "Eden Zamir", + "email": "eden.zamir@example.com", + "age": 29, + "city": "Tel Aviv", + } + + user3 := map[string]interface{}{ + "name": "Paul Zamir", + "email": "paul.zamir@example.com", + "age": 35, + "city": "Tel Aviv", + } + + // STEP_START add_hash_data + _, err = rdb.HSet(ctx, "huser:1", user1).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.HSet(ctx, "huser:2", user2).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.HSet(ctx, "huser:3", user3).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START query1_hash + findPaulHashResult, err := rdb.FTSearch( + ctx, + "hash-idx:users", + "Paul @age:[30 40]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(findPaulHashResult) + // >>> {1 [{huser:3 map[age:35 city:Tel Aviv... + // STEP_END + + // Output: + // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir]}]} +} From 4f43d867136f3e8da5dd48d10f440ae88724f437 Mon Sep 17 00:00:00 2001 From: frankj Date: Wed, 23 Apr 2025 15:47:13 +0800 Subject: [PATCH 156/230] fix: Fix panic caused when arg is nil (#3353) --- commands.go | 2 ++ commands_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/commands.go b/commands.go index 123005bc79..bca7d7eedd 100644 --- a/commands.go +++ b/commands.go @@ -81,6 +81,8 @@ func appendArg(dst []interface{}, arg interface{}) []interface{} { return dst case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP: return append(dst, arg) + case nil: + return dst default: // scan struct field v := reflect.ValueOf(arg) diff --git a/commands_test.go b/commands_test.go index 55b9574964..6a76756a98 100644 --- a/commands_test.go +++ b/commands_test.go @@ -7209,6 +7209,17 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(vals).To(Equal([]interface{}{int64(12), proto.RedisError("error"), "abc"})) }) + + It("returns empty values when args are nil", func() { + vals, err := client.Eval( + ctx, + "return {ARGV[1]}", + []string{}, + nil, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(vals).To(BeEmpty()) + }) }) Describe("EvalRO", func() { @@ -7232,6 +7243,17 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(vals).To(Equal([]interface{}{int64(12), proto.RedisError("error"), "abc"})) }) + + It("returns empty values when args are nil", func() { + vals, err := client.EvalRO( + ctx, + "return {ARGV[1]}", + []string{}, + nil, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(vals).To(BeEmpty()) + }) }) Describe("Functions", func() { From 462175e3749fa6c19ec2b598efc16ffdd887eb3d Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:36:38 +0300 Subject: [PATCH 157/230] Update README.md, use redis discord guild (#3331) * use redis discord guild * add line in CONTRIBUTING.md * update with badges similar to rest of the libraries. update url * updated with direct invite link * fix discord link in CONTRIBUTING.md * fix stackoverflow tag --------- Co-authored-by: Elena Kolevska --- CONTRIBUTING.md | 4 ++++ README.md | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcaee7c731..7228a4a060 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,3 +112,7 @@ The core team regularly looks at pull requests. We will provide feedback as soon as possible. After receiving our feedback, please respond within two weeks. After that time, we may close your PR if it isn't showing any activity. + +## Support + +Maintainers can provide limited support to contributors on discord: https://discord.gg/W4txy5AeKM diff --git a/README.md b/README.md index 6c42c0fc2c..4487c6e9a7 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,14 @@ [![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc) [![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/) +[![Go Report Card](https://goreportcard.com/badge/github.com/redis/go-redis/v9)](https://goreportcard.com/report/github.com/redis/go-redis/v9) [![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) -[![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj) + +[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/W4txy5AeKM) +[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc) +[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc) +[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) +[![Stack Exchange questions](https://img.shields.io/stackexchange/stackoverflow/t/go-redis?style=social&logo=stackoverflow&label=Stackoverflow)](https://stackoverflow.com/questions/tagged/go-redis) > go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers. @@ -44,7 +50,7 @@ in the `go.mod` to `go 1.24` in one of the next releases. ## Resources - [Discussions](https://github.com/redis/go-redis/discussions) -- [Chat](https://discord.gg/rWtp5Aj) +- [Chat](https://discord.gg/W4txy5AeKM) - [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9) - [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples) From b3880883b42130e75eb7f0c609c3c6207375cddc Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 23 Apr 2025 22:52:38 +0300 Subject: [PATCH 158/230] update HExpire command documentation (#3355) * update HExpire command documentation * Apply suggestions from code review Format the links in the documentation. Add missing documentation. --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- hash_commands.go | 54 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/hash_commands.go b/hash_commands.go index be58b8d2d0..98a361b3ef 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -224,7 +224,10 @@ type HExpireArgs struct { // HExpire - Sets the expiration time for specified fields in a hash in seconds. // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. -// For more information - https://redis.io/commands/hexpire/ +// Available since Redis 7.4 CE. +// For more information refer to [HEXPIRE Documentation]. +// +// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration), "FIELDS", len(fields)} @@ -239,7 +242,10 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati // HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds. // It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. -// For more information - https://redis.io/commands/hexpire/ +// Available since Redis 7.4 CE. +// For more information refer to [HEXPIRE Documentation]. +// +// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/ func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration)} @@ -268,7 +274,10 @@ func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration tim // HPExpire - Sets the expiration time for specified fields in a hash in milliseconds. // Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields. // The command modifies the standard time.Duration to milliseconds for the Redis command. -// For more information - https://redis.io/commands/hpexpire/ +// Available since Redis 7.4 CE. +// For more information refer to [HPEXPIRE Documentation]. +// +// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/ func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)} @@ -280,6 +289,13 @@ func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Durat return cmd } +// HPExpireWithArgs - Sets the expiration time for specified fields in a hash in milliseconds. +// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. +// The command constructs an argument list starting with "HPEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. +// Available since Redis 7.4 CE. +// For more information refer to [HPEXPIRE Documentation]. +// +// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/ func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)} @@ -308,7 +324,10 @@ func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration ti // HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds. // Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields. // The command sets absolute expiration times based on the UNIX timestamp provided. -// For more information - https://redis.io/commands/hexpireat/ +// Available since Redis 7.4 CE. +// For more information refer to [HExpireAt Documentation]. +// +// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/ func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)} @@ -348,7 +367,10 @@ func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time // HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds. // Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds. -// For more information - https://redis.io/commands/hpexpireat/ +// Available since Redis 7.4 CE. +// For more information refer to [HExpireAt Documentation]. +// +// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/ func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)} @@ -388,7 +410,10 @@ func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Tim // HPersist - Removes the expiration time from specified fields in a hash. // Accepts a key and the fields themselves. // This command ensures that each field specified will have its expiration removed if present. -// For more information - https://redis.io/commands/hpersist/ +// Available since Redis 7.4 CE. +// For more information refer to [HPersist Documentation]. +// +// [HPersist Documentation]: https://redis.io/commands/hpersist/ func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd { args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)} @@ -403,6 +428,10 @@ func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *In // HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds. // Requires a key and the fields themselves to fetch their expiration timestamps. // This command returns the expiration times for each field or error/status codes for each field as specified. +// Available since Redis 7.4 CE. +// For more information refer to [HExpireTime Documentation]. +// +// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/ // For more information - https://redis.io/commands/hexpiretime/ func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)} @@ -418,6 +447,10 @@ func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) // HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds. // Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters. // Provides the expiration timestamp for each field in milliseconds. +// Available since Redis 7.4 CE. +// For more information refer to [HExpireTime Documentation]. +// +// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/ // For more information - https://redis.io/commands/hexpiretime/ func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)} @@ -433,7 +466,10 @@ func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) // HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds. // Requires a key and the fields themselves. It returns the TTL for each specified field. // This command fetches the TTL in seconds for each field or returns error/status codes as appropriate. -// For more information - https://redis.io/commands/httl/ +// Available since Redis 7.4 CE. +// For more information refer to [HTTL Documentation]. +// +// [HTTL Documentation]: https://redis.io/commands/httl/ func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { args := []interface{}{"HTTL", key, "FIELDS", len(fields)} @@ -448,6 +484,10 @@ func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSli // HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds. // Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields. // This command provides the TTL in milliseconds for each field or returns error/status codes as needed. +// Available since Redis 7.4 CE. +// For more information refer to [HPTTL Documentation]. +// +// [HPTTL Documentation]: https://redis.io/commands/hpttl/ // For more information - https://redis.io/commands/hpttl/ func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { args := []interface{}{"HPTTL", key, "FIELDS", len(fields)} From b1ba4f7b27703d84704d810a829d75e27fcaefd7 Mon Sep 17 00:00:00 2001 From: Hui Date: Tue, 29 Apr 2025 05:16:53 +0800 Subject: [PATCH 159/230] feat: func isEmptyValue support time.Time (#3273) * fix:func isEmptyValue support time.Time * fix: Improve HSet unit tests * feat: Improve HSet unit tests * fix: isEmptyValue Struct only support time.Time * test(hset): add empty custom struct test --------- Co-authored-by: Guo Hui Co-authored-by: Nedyalko Dyakov --- commands.go | 6 +++++ commands_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/commands.go b/commands.go index bca7d7eedd..2713232420 100644 --- a/commands.go +++ b/commands.go @@ -155,6 +155,12 @@ func isEmptyValue(v reflect.Value) bool { return v.Float() == 0 case reflect.Interface, reflect.Pointer: return v.IsNil() + case reflect.Struct: + if v.Type() == reflect.TypeOf(time.Time{}) { + return v.IsZero() + } + // Only supports the struct time.Time, + // subsequent iterations will follow the func Scan support decoder. } return false } diff --git a/commands_test.go b/commands_test.go index 6a76756a98..8b2aa37d47 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2578,6 +2578,63 @@ var _ = Describe("Commands", func() { "val2", "val", })) + + type setOmitEmpty struct { + Set1 string `redis:"set1"` + Set2 int `redis:"set2,omitempty"` + Set3 time.Duration `redis:"set3,omitempty"` + Set4 string `redis:"set4,omitempty"` + Set5 time.Time `redis:"set5,omitempty"` + Set6 *numberStruct `redis:"set6,omitempty"` + Set7 numberStruct `redis:"set7,omitempty"` + } + + hSet = client.HSet(ctx, "hash3", &setOmitEmpty{ + Set1: "val", + }) + Expect(hSet.Err()).NotTo(HaveOccurred()) + // both set1 and set7 are set + // custom struct is not omitted + Expect(hSet.Val()).To(Equal(int64(2))) + + hGetAll := client.HGetAll(ctx, "hash3") + Expect(hGetAll.Err()).NotTo(HaveOccurred()) + Expect(hGetAll.Val()).To(Equal(map[string]string{ + "set1": "val", + "set7": `{"Number":0}`, + })) + var hash3 setOmitEmpty + Expect(hGetAll.Scan(&hash3)).NotTo(HaveOccurred()) + Expect(hash3.Set1).To(Equal("val")) + Expect(hash3.Set2).To(Equal(0)) + Expect(hash3.Set3).To(Equal(time.Duration(0))) + Expect(hash3.Set4).To(Equal("")) + Expect(hash3.Set5).To(Equal(time.Time{})) + Expect(hash3.Set6).To(BeNil()) + Expect(hash3.Set7).To(Equal(numberStruct{})) + + now := time.Now() + hSet = client.HSet(ctx, "hash4", setOmitEmpty{ + Set1: "val", + Set5: now, + Set6: &numberStruct{ + Number: 5, + }, + Set7: numberStruct{ + Number: 3, + }, + }) + Expect(hSet.Err()).NotTo(HaveOccurred()) + Expect(hSet.Val()).To(Equal(int64(4))) + + hGetAll = client.HGetAll(ctx, "hash4") + Expect(hGetAll.Err()).NotTo(HaveOccurred()) + Expect(hGetAll.Val()).To(Equal(map[string]string{ + "set1": "val", + "set5": now.Format(time.RFC3339Nano), + "set6": `{"Number":5}`, + "set7": `{"Number":3}`, + })) }) It("should HSetNX", func() { @@ -7619,12 +7676,16 @@ type numberStruct struct { Number int } -func (s *numberStruct) MarshalBinary() ([]byte, error) { - return json.Marshal(s) +func (n numberStruct) MarshalBinary() ([]byte, error) { + return json.Marshal(n) +} + +func (n *numberStruct) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, n) } -func (s *numberStruct) UnmarshalBinary(b []byte) error { - return json.Unmarshal(b, s) +func (n *numberStruct) ScanRedis(str string) error { + return json.Unmarshal([]byte(str), n) } func deref(viface interface{}) interface{} { From bfcf5171c1e27160057ef20368d93078d4a34fd7 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 29 Apr 2025 14:39:26 +0800 Subject: [PATCH 160/230] fix: `PubSub` isn't concurrency-safe (#3360) --- pubsub.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pubsub.go b/pubsub.go index 20c085f1f8..2a0e7a81e1 100644 --- a/pubsub.go +++ b/pubsub.go @@ -45,6 +45,9 @@ func (c *PubSub) init() { } func (c *PubSub) String() string { + c.mu.Lock() + defer c.mu.Unlock() + channels := mapKeys(c.channels) channels = append(channels, mapKeys(c.patterns)...) channels = append(channels, mapKeys(c.schannels)...) From 55758375fcacab4b27fc3761b604ea012c1b5099 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:53:06 +0300 Subject: [PATCH 161/230] migrate golangci-lint config to v2 format (#3354) * migrate golangci-lint config to v2 format * chore: skip CI on migration [skip ci] * Bump golangci version * Address several golangci-lint/staticcheck warnings * change staticchecks settings --- .github/workflows/golangci-lint.yml | 4 ++-- .golangci.yml | 31 +++++++++++++++++++++++++++++ command.go | 5 +++-- extra/rediscensus/go.mod | 2 +- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 2 +- extra/redisprometheus/go.mod | 2 +- options.go | 5 +++-- osscluster.go | 5 +++-- ring.go | 5 +++-- sentinel_test.go | 2 +- universal.go | 14 ++++++------- 12 files changed, 57 insertions(+), 22 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 515750af60..5e0ac1d0ac 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.5.2 + uses: golangci/golangci-lint-action@v7.0.0 with: - verify: false # disable verifying the configuration since golangci is currently introducing breaking changes in the configuration + verify: true diff --git a/.golangci.yml b/.golangci.yml index 285aca6b3a..872454ff7f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,34 @@ +version: "2" run: timeout: 5m tests: false +linters: + settings: + staticcheck: + checks: + - all + # Incorrect or missing package comment. + # https://staticcheck.dev/docs/checks/#ST1000 + - -ST1000 + # Omit embedded fields from selector expression. + # https://staticcheck.dev/docs/checks/#QF1008 + - -QF1008 + - -ST1003 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/command.go b/command.go index 364706e3e7..3253af6cc9 100644 --- a/command.go +++ b/command.go @@ -1412,7 +1412,8 @@ func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { cmd.val = make(map[string][]interface{}) - if readType == proto.RespMap { + switch readType { + case proto.RespMap: n, err := rd.ReadMapLen() if err != nil { return err @@ -1435,7 +1436,7 @@ func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { cmd.val[k][j] = value } } - } else if readType == proto.RespArray { + case proto.RespArray: // RESP2 response n, err := rd.ReadArrayLen() if err != nil { diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 7033e805f6..b39f7dd4c4 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -19,6 +19,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. v9.7.2 // This version was accidentally released. + v9.5.3 // This version was accidentally released. ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index c1cff3e90d..93cc423dba 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -16,6 +16,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. v9.7.2 // This version was accidentally released. + v9.5.3 // This version was accidentally released. ) diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index e5b442e614..c5b29dffa1 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -24,6 +24,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. v9.7.2 // This version was accidentally released. + v9.5.3 // This version was accidentally released. ) diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 8bff000869..c934767e05 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -23,6 +23,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. v9.7.2 // This version was accidentally released. + v9.5.3 // This version was accidentally released. ) diff --git a/options.go b/options.go index 0ebeec342c..3ffcd07ede 100644 --- a/options.go +++ b/options.go @@ -214,9 +214,10 @@ func (opt *Options) init() { opt.ConnMaxIdleTime = 30 * time.Minute } - if opt.MaxRetries == -1 { + switch opt.MaxRetries { + case -1: opt.MaxRetries = 0 - } else if opt.MaxRetries == 0 { + case 0: opt.MaxRetries = 3 } switch opt.MinRetryBackoff { diff --git a/osscluster.go b/osscluster.go index 20180464e2..3b46cbe35b 100644 --- a/osscluster.go +++ b/osscluster.go @@ -111,9 +111,10 @@ type ClusterOptions struct { } func (opt *ClusterOptions) init() { - if opt.MaxRedirects == -1 { + switch opt.MaxRedirects { + case -1: opt.MaxRedirects = 0 - } else if opt.MaxRedirects == 0 { + case 0: opt.MaxRedirects = 3 } diff --git a/ring.go b/ring.go index 0ff3f75b1e..8f2dd3c401 100644 --- a/ring.go +++ b/ring.go @@ -128,9 +128,10 @@ func (opt *RingOptions) init() { opt.NewConsistentHash = newRendezvous } - if opt.MaxRetries == -1 { + switch opt.MaxRetries { + case -1: opt.MaxRetries = 0 - } else if opt.MaxRetries == 0 { + case 0: opt.MaxRetries = 3 } switch opt.MinRetryBackoff { diff --git a/sentinel_test.go b/sentinel_test.go index cde7f956d1..2d481d5fc3 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -41,7 +41,7 @@ var _ = Describe("Sentinel resolution", func() { client := redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: sentinelName, SentinelAddrs: sentinelAddrs, - MaxRetries: -1, + MaxRetries: -1, }) err := client.Ping(shortCtx).Err() diff --git a/universal.go b/universal.go index 3d91dd493e..46d5640da6 100644 --- a/universal.go +++ b/universal.go @@ -259,13 +259,13 @@ var ( // NewUniversalClient returns a new multi client. The type of the returned client depends // on the following conditions: // -// 1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode, -// a FailoverClusterClient is returned. -// 2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode, -// a sentinel-backed FailoverClient is returned. -// 3. If the number of Addrs is two or more, or IsClusterMode option is specified, -// a ClusterClient is returned. -// 4. Otherwise, a single-node Client is returned. +// 1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode, +// a FailoverClusterClient is returned. +// 2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode, +// a sentinel-backed FailoverClient is returned. +// 3. If the number of Addrs is two or more, or IsClusterMode option is specified, +// a ClusterClient is returned. +// 4. Otherwise, a single-node Client is returned. func NewUniversalClient(opts *UniversalOptions) UniversalClient { switch { case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode): From b583d957f1799dbd5fdc2c3e32407492c5878860 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:08:34 +0300 Subject: [PATCH 162/230] chore(ci): Use redis 8 rc2 image. (#3361) * chore(ci): Use redis 8 rc2 image * test(timeseries): fix duplicatePolicy check --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- timeseries_commands_test.go | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 2edb16d398..08323aa511 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC1"]="8.0-RC1-pre" + ["8.0-RC2"]="8.0-RC2-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f88ca67224..810ab50979 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC1" # 8.0 RC1 + - "8.0-RC2" # 8.0 RC2 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC1"]="8.0-RC1-pre" + ["8.0-RC2"]="8.0-RC2-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC1" # 8.0 RC1 + - "8.0-RC2" # 8.0 RC2 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/timeseries_commands_test.go b/timeseries_commands_test.go index d0d865b48c..fdef3e6029 100644 --- a/timeseries_commands_test.go +++ b/timeseries_commands_test.go @@ -269,11 +269,21 @@ var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { if client.Options().Protocol == 2 { Expect(resultInfo["labels"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"Time", "Series"})) Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + if RedisVersion >= 8 { + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("block")) + } else { + // Older versions of Redis had a bug where the duplicate policy was not set correctly + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } } else { Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + if RedisVersion >= 8 { + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("block")) + } else { + // Older versions of Redis had a bug where the duplicate policy was not set correctly + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } } opt = &redis.TSAlterOptions{DuplicatePolicy: "min"} resultAlter, err = client.TSAlter(ctx, "1", opt).Result() From 64390f210ea4dfff4ae024de627002c05cdb4820 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:33:40 +0300 Subject: [PATCH 163/230] feat(options): panic when options are nil (#3363) Client creation should panic when options are nil. --- osscluster.go | 3 +++ redis.go | 3 +++ redis_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ ring.go | 3 +++ sentinel.go | 11 +++++++++++ universal.go | 4 ++++ 6 files changed, 75 insertions(+) diff --git a/osscluster.go b/osscluster.go index 3b46cbe35b..c0278ed057 100644 --- a/osscluster.go +++ b/osscluster.go @@ -924,6 +924,9 @@ type ClusterClient struct { // NewClusterClient returns a Redis Cluster client as described in // http://redis.io/topics/cluster-spec. func NewClusterClient(opt *ClusterOptions) *ClusterClient { + if opt == nil { + panic("redis: NewClusterClient nil options") + } opt.init() c := &ClusterClient{ diff --git a/redis.go b/redis.go index e0159294dd..f50df5689b 100644 --- a/redis.go +++ b/redis.go @@ -661,6 +661,9 @@ type Client struct { // NewClient returns a client to the Redis Server specified by Options. func NewClient(opt *Options) *Client { + if opt == nil { + panic("redis: NewClient nil options") + } opt.init() c := Client{ diff --git a/redis_test.go b/redis_test.go index 7d9bf1cef9..80e28341c1 100644 --- a/redis_test.go +++ b/redis_test.go @@ -727,3 +727,54 @@ var _ = Describe("Dialer connection timeouts", func() { Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay)) }) }) +var _ = Describe("Client creation", func() { + Context("simple client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewClient(nil) + }).To(Panic()) + }) + }) + Context("cluster client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewClusterClient(nil) + }).To(Panic()) + }) + }) + Context("ring client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewRing(nil) + }).To(Panic()) + }) + }) + Context("universal client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewUniversalClient(nil) + }).To(Panic()) + }) + }) + Context("failover client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewFailoverClient(nil) + }).To(Panic()) + }) + }) + Context("failover cluster client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewFailoverClusterClient(nil) + }).To(Panic()) + }) + }) + Context("sentinel client with nil options", func() { + It("panics", func() { + Expect(func() { + redis.NewSentinelClient(nil) + }).To(Panic()) + }) + }) +}) diff --git a/ring.go b/ring.go index 8f2dd3c401..555ea2a163 100644 --- a/ring.go +++ b/ring.go @@ -523,6 +523,9 @@ type Ring struct { } func NewRing(opt *RingOptions) *Ring { + if opt == nil { + panic("redis: NewRing nil options") + } opt.init() hbCtx, hbCancel := context.WithCancel(context.Background()) diff --git a/sentinel.go b/sentinel.go index f5b9a52d10..cfc848cf0c 100644 --- a/sentinel.go +++ b/sentinel.go @@ -224,6 +224,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { // for automatic failover. It's safe for concurrent use by multiple // goroutines. func NewFailoverClient(failoverOpt *FailoverOptions) *Client { + if failoverOpt == nil { + panic("redis: NewFailoverClient nil options") + } + if failoverOpt.RouteByLatency { panic("to route commands by latency, use NewFailoverClusterClient") } @@ -313,6 +317,9 @@ type SentinelClient struct { } func NewSentinelClient(opt *Options) *SentinelClient { + if opt == nil { + panic("redis: NewSentinelClient nil options") + } opt.init() c := &SentinelClient{ baseClient: &baseClient{ @@ -828,6 +835,10 @@ func contains(slice []string, str string) bool { // NewFailoverClusterClient returns a client that supports routing read-only commands // to a replica node. func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient { + if failoverOpt == nil { + panic("redis: NewFailoverClusterClient nil options") + } + sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs)) copy(sentinelAddrs, failoverOpt.SentinelAddrs) diff --git a/universal.go b/universal.go index 46d5640da6..a1ce17bac3 100644 --- a/universal.go +++ b/universal.go @@ -267,6 +267,10 @@ var ( // a ClusterClient is returned. // 4. Otherwise, a single-node Client is returned. func NewUniversalClient(opts *UniversalOptions) UniversalClient { + if opts == nil { + panic("redis: NewUniversalClient nil options") + } + switch { case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode): return NewFailoverClusterClient(opts.Failover()) From 050bc05ba0a7f77dc43099fc22b806ddc78e9728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 08:34:14 +0300 Subject: [PATCH 164/230] chore(deps): bump golangci/golangci-lint-action from 7.0.0 to 8.0.0 (#3366) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7.0.0 to 8.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v7.0.0...v8.0.0) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5e0ac1d0ac..8d4135d5ad 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v7.0.0 + uses: golangci/golangci-lint-action@v8.0.0 with: verify: true From f1ca48606208c117b268c962ef99da9d60c7802a Mon Sep 17 00:00:00 2001 From: "fengyun.rui" Date: Wed, 7 May 2025 16:14:48 +0800 Subject: [PATCH 165/230] feat: add more stats for otel (#2930) Signed-off-by: rfyiamcool Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- extra/redisotel/metrics.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/extra/redisotel/metrics.go b/extra/redisotel/metrics.go index 915838f346..4974f4e8de 100644 --- a/extra/redisotel/metrics.go +++ b/extra/redisotel/metrics.go @@ -127,6 +127,22 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { return err } + hits, err := conf.meter.Int64ObservableUpDownCounter( + "db.client.connections.hits", + metric.WithDescription("The number of times free connection was found in the pool"), + ) + if err != nil { + return err + } + + misses, err := conf.meter.Int64ObservableUpDownCounter( + "db.client.connections.misses", + metric.WithDescription("The number of times free connection was not found in the pool"), + ) + if err != nil { + return err + } + redisConf := rdb.Options() _, err = conf.meter.RegisterCallback( func(ctx context.Context, o metric.Observer) error { @@ -140,6 +156,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributes(usedAttrs...)) o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributes(labels...)) + o.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributes(labels...)) + o.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributes(labels...)) return nil }, idleMax, @@ -147,6 +165,8 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { connsMax, usage, timeouts, + hits, + misses, ) return err From 8e010d037c0a9861b7ba035f5610b64da6329a49 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 7 May 2025 14:40:49 +0300 Subject: [PATCH 166/230] chore(release): sync master after releasing V9.8.0 (#3365) * Bump version to 9.8.0-beta1 Update README.md * Feature more prominently how to enable OpenTelemetry instrumentation (#3316) * Sync master with v9.8.0-beta.1 (#3322) * DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush (#3234) * DOC-4464 examples for llen, lpop, lpush, lrange, rpop, and rpush * DOC-4464 improved variable names --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov * update pubsub.go (#3329) * use 8.0-RC1 (#3330) * drop ft.profile that was never enabled (#3323) * chore(deps): bump rojopolis/spellcheck-github-actions (#3336) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.47.0 to 0.48.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.47.0...0.48.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-version: 0.48.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix FT.Search Limit argument and add CountOnly argument for limit 0 0 (#3338) * Fix Limit argument and add CountOnly argument * Add test and Documentation * Update search_commands.go --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * fix add missing command in interface (#3344) * Use DB option in NewFailoverClusterClient (#3342) * DOC-5102 added CountOnly search example for docs (#3345) * Add integration tests for Redis 8 behavior changes in Redis Search (#3337) * Add integration tests for Redis 8 behavior changes in Redis Search * Undo changes in ft.search limit * Fix BM25 as the default scorer test * Add more tests and comments on deprecated params * Update search_commands.go * Remove deprication comment for nostopwords --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * Use correct slot for COUNTKEYSINSLOT command (#3327) * Ensure context isn't exhausted via concurrent query as opposed to sentinel query (#3334) * fix: better error handling when fetching the master node from the sentinels (#3349) * Better error handling when fetching the master node from the sentinels * fix error message generation * close the errCh to not block * use len over errCh * docs: fix documentation comments (#3351) * DOC-5111 added hash search examples (#3357) * fix: Fix panic caused when arg is nil (#3353) * Update README.md, use redis discord guild (#3331) * use redis discord guild * add line in CONTRIBUTING.md * update with badges similar to rest of the libraries. update url * updated with direct invite link * fix discord link in CONTRIBUTING.md * fix stackoverflow tag --------- Co-authored-by: Elena Kolevska * update HExpire command documentation (#3355) * update HExpire command documentation * Apply suggestions from code review Format the links in the documentation. Add missing documentation. --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * feat: func isEmptyValue support time.Time (#3273) * fix:func isEmptyValue support time.Time * fix: Improve HSet unit tests * feat: Improve HSet unit tests * fix: isEmptyValue Struct only support time.Time * test(hset): add empty custom struct test --------- Co-authored-by: Guo Hui Co-authored-by: Nedyalko Dyakov * fix: `PubSub` isn't concurrency-safe (#3360) * migrate golangci-lint config to v2 format (#3354) * migrate golangci-lint config to v2 format * chore: skip CI on migration [skip ci] * Bump golangci version * Address several golangci-lint/staticcheck warnings * change staticchecks settings * chore(ci): Use redis 8 rc2 image. (#3361) * chore(ci): Use redis 8 rc2 image * test(timeseries): fix duplicatePolicy check * feat(options): panic when options are nil (#3363) Client creation should panic when options are nil. * chore(release): Update version to v9.8.0 - update version in relevant places - add RELEASE-NOTES.md to keep track of release notes --------- Signed-off-by: dependabot[bot] Co-authored-by: Nikolay Dubina Co-authored-by: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: Liu Shuang Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Bulat Khasanov Co-authored-by: Naveen Prashanth <78990165+gnpaone@users.noreply.github.com> Co-authored-by: Glenn Co-authored-by: frankj Co-authored-by: Elena Kolevska Co-authored-by: Hui Co-authored-by: Guo Hui Co-authored-by: fukua95 --- .github/workflows/build.yml | 4 +- .github/workflows/codeql-analysis.yml | 5 +- .github/workflows/golangci-lint.yml | 1 + .github/workflows/test-redis-enterprise.yml | 2 +- RELEASE-NOTES.md | 80 +++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 +- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 +- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 17 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 RELEASE-NOTES.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 810ab50979..a58ebb9c8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9, v9.7] + branches: [master, v9, v9.7, v9.8] pull_request: - branches: [master, v9, v9.7] + branches: [master, v9, v9.7, v9.8] permissions: contents: read diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c4b558f37a..1a803d373b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,9 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [master, v9, v9.7, v9.8] pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master, v9, v9.7, v9.8] jobs: analyze: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 8d4135d5ad..def3eb7945 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -8,6 +8,7 @@ on: - master - main - v9 + - v9.8 pull_request: permissions: diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 459b2edf00..47de6478cb 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -2,7 +2,7 @@ name: RE Tests on: push: - branches: [master] + branches: [master, v9, v9.7, v9.8] pull_request: permissions: diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 0000000000..fa106cb924 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,80 @@ +# Release Notes + +# 9.8.0 (2025-04-30) + +## 🚀 Highlights +- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration +- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command +- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search` + +## ✨ New Features +- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305)) +- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843)) +- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182)) +- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273)) +- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794)) +- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255)) +- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294)) +- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) +- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281)) +- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338)) +- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342)) +- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363)) + +## 🐛 Bug Fixes +- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360)) +- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353)) +- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349)) +- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298)) +- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190)) +- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164)) +- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290)) +- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168)) +- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338)) +- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344)) +- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327)) +- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329)) + +## 📚 Documentation +- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357)) +- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351)) +- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345)) +- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234)) +- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242)) +- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331)) +- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355)) +- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316)) +- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55)) + +## ⚡ Performance and Reliability +- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089)) +- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334)) + +## 🔧 Dependencies and Infrastructure +- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361)) +- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274)) +- Updated various dependencies: + - Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354)) + - Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336)) + - Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308)) +- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354)) + +## ⚠️ Breaking Changes +- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213)) +- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321)) +- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323)) + +## 🔒 Security +- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295)) + +## 🧪 Testing +- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337)) +- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299)) +- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285)) +- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292)) + +## 👥 Contributors + +We would like to thank all the contributors who made this release possible: + +[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 727fbbd7fd..6d731f370d 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 775e3e7b19..28edd2caf2 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index 33d3ef6d80..c10579f13e 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 363c93c294..50964fa8ea 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 5a060d99a7..da45631037 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 3d7a4caaa8..86f25db7b6 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0-beta.1 +require github.com/redis/go-redis/v9 v9.8.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 33d3ef6d80..c10579f13e 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index b39f7dd4c4..65499e2c7c 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 93cc423dba..20c78b9d15 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index c5b29dffa1..c9c63427b5 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0-beta.1 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.8.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index c934767e05..25193e4655 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.8.0-beta.1 + github.com/redis/go-redis/v9 v9.8.0 ) require ( diff --git a/version.go b/version.go index b547951687..c56e04ff14 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.8.0-beta.1" + return "9.8.0" } From 9db5abe5be95fffdba0b68ac463a7ae98081a085 Mon Sep 17 00:00:00 2001 From: Lev Zakharov Date: Wed, 7 May 2025 15:54:26 +0300 Subject: [PATCH 167/230] feat: add connection waiting statistics (#2804) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- internal/pool/pool.go | 22 +++++++++++++------- internal/pool/pool_test.go | 41 ++++++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/internal/pool/pool.go b/internal/pool/pool.go index b69c75f4f0..e7d951e268 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -33,9 +33,11 @@ var timers = sync.Pool{ // Stats contains pool state information and accumulated stats. type Stats struct { - Hits uint32 // number of times free connection was found in the pool - Misses uint32 // number of times free connection was NOT found in the pool - Timeouts uint32 // number of times a wait timeout occurred + Hits uint32 // number of times free connection was found in the pool + Misses uint32 // number of times free connection was NOT found in the pool + Timeouts uint32 // number of times a wait timeout occurred + WaitCount uint32 // number of times a connection was waited + WaitDurationNs int64 // total time spent for waiting a connection in nanoseconds TotalConns uint32 // number of total connections in the pool IdleConns uint32 // number of idle connections in the pool @@ -90,7 +92,8 @@ type ConnPool struct { poolSize int idleConnsLen int - stats Stats + stats Stats + waitDurationNs atomic.Int64 _closed uint32 // atomic } @@ -320,6 +323,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { default: } + start := time.Now() timer := timers.Get().(*time.Timer) timer.Reset(p.cfg.PoolTimeout) @@ -331,6 +335,8 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { timers.Put(timer) return ctx.Err() case p.queue <- struct{}{}: + p.waitDurationNs.Add(time.Since(start).Nanoseconds()) + atomic.AddUint32(&p.stats.WaitCount, 1) if !timer.Stop() { <-timer.C } @@ -457,9 +463,11 @@ func (p *ConnPool) IdleLen() int { func (p *ConnPool) Stats() *Stats { return &Stats{ - Hits: atomic.LoadUint32(&p.stats.Hits), - Misses: atomic.LoadUint32(&p.stats.Misses), - Timeouts: atomic.LoadUint32(&p.stats.Timeouts), + Hits: atomic.LoadUint32(&p.stats.Hits), + Misses: atomic.LoadUint32(&p.stats.Misses), + Timeouts: atomic.LoadUint32(&p.stats.Timeouts), + WaitCount: atomic.LoadUint32(&p.stats.WaitCount), + WaitDurationNs: p.waitDurationNs.Load(), TotalConns: uint32(p.Len()), IdleConns: uint32(p.IdleLen()), diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 99f31bd74d..d198ba5400 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -59,12 +59,14 @@ var _ = Describe("ConnPool", func() { time.Sleep(time.Second) Expect(connPool.Stats()).To(Equal(&pool.Stats{ - Hits: 0, - Misses: 0, - Timeouts: 0, - TotalConns: 0, - IdleConns: 0, - StaleConns: 0, + Hits: 0, + Misses: 0, + Timeouts: 0, + WaitCount: 0, + WaitDurationNs: 0, + TotalConns: 0, + IdleConns: 0, + StaleConns: 0, })) }) @@ -358,4 +360,31 @@ var _ = Describe("race", func() { Expect(stats.IdleConns).To(Equal(uint32(0))) Expect(stats.TotalConns).To(Equal(uint32(opt.PoolSize))) }) + + It("wait", func() { + opt := &pool.Options{ + Dialer: func(ctx context.Context) (net.Conn, error) { + return &net.TCPConn{}, nil + }, + PoolSize: 1, + PoolTimeout: 3 * time.Second, + } + p := pool.NewConnPool(opt) + + wait := make(chan struct{}) + conn, _ := p.Get(ctx) + go func() { + _, _ = p.Get(ctx) + wait <- struct{}{} + }() + time.Sleep(time.Second) + p.Put(ctx, conn) + <-wait + + stats := p.Stats() + Expect(stats.IdleConns).To(Equal(uint32(0))) + Expect(stats.TotalConns).To(Equal(uint32(1))) + Expect(stats.WaitCount).To(Equal(uint32(1))) + Expect(stats.WaitDurationNs).To(BeNumerically("~", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds())) + }) }) From 289744d38da8227558eb401d1d8b1166ae817bd9 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Thu, 8 May 2025 15:32:47 +0300 Subject: [PATCH 168/230] ci(redis): update to 8.0.1 (#3372) --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- main_test.go | 3 ++- search_test.go | 7 +++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 08323aa511..0696f38de6 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC2"]="8.0-RC2-pre" + ["8.0.1"]="8.0.1-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a58ebb9c8c..bde6cc721d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC2" # 8.0 RC2 + - "8.0.1" # 8.0.1 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-RC2"]="8.0-RC2-pre" + ["8.0.1"]="8.0.1-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-RC2" # 8.0 RC2 + - "8.0.1" # 8.0.1 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/main_test.go b/main_test.go index 556e633e53..29e6014b9b 100644 --- a/main_test.go +++ b/main_test.go @@ -100,7 +100,8 @@ var _ = BeforeSuite(func() { fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_VERSION: %v\n", RedisVersion) + fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion) + fmt.Printf("CLIENT_LIBS_TEST_IMAGE: %v\n", os.Getenv("CLIENT_LIBS_TEST_IMAGE")) if RedisVersion < 7.0 || RedisVersion > 9 { panic("incorrect or not supported redis version") diff --git a/search_test.go b/search_test.go index 6bc8b11123..019acbe338 100644 --- a/search_test.go +++ b/search_test.go @@ -1871,17 +1871,20 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "aggTimeoutHeavy") - const totalDocs = 10000 + const totalDocs = 100000 for i := 0; i < totalDocs; i++ { key := fmt.Sprintf("doc%d", i) _, err := client.HSet(ctx, key, "n", i).Result() Expect(err).NotTo(HaveOccurred()) } + // default behaviour was changed in 8.0.1, set to fail to validate the timeout was triggered + err = client.ConfigSet(ctx, "search-on-timeout", "fail").Err() + Expect(err).NotTo(HaveOccurred()) options := &redis.FTAggregateOptions{ SortBy: []redis.FTAggregateSortBy{{FieldName: "@n", Desc: true}}, LimitOffset: 0, - Limit: 100, + Limit: 100000, Timeout: 1, // 1 ms timeout, expected to trigger a timeout error. } _, err = client.FTAggregateWithArgs(ctx, "aggTimeoutHeavy", "*", options).Result() From a8e958b732f663be949275588cb4b08c61c759b2 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Fri, 9 May 2025 12:24:36 +0300 Subject: [PATCH 169/230] utils: export ParseFloat and MustParseFloat wrapping internal utils (#3371) * utils: expose ParseFloat via new public utils package * add tests for special float values in vector search --- helper/helper.go | 11 ++++++ internal/util/convert.go | 30 +++++++++++++++ internal/util/convert_test.go | 40 ++++++++++++++++++++ search_test.go | 71 ++++++++++++++++++++++++++++++++--- 4 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 helper/helper.go create mode 100644 internal/util/convert.go create mode 100644 internal/util/convert_test.go diff --git a/helper/helper.go b/helper/helper.go new file mode 100644 index 0000000000..7047c8ae91 --- /dev/null +++ b/helper/helper.go @@ -0,0 +1,11 @@ +package helper + +import "github.com/redis/go-redis/v9/internal/util" + +func ParseFloat(s string) (float64, error) { + return util.ParseStringToFloat(s) +} + +func MustParseFloat(s string) float64 { + return util.MustParseFloat(s) +} diff --git a/internal/util/convert.go b/internal/util/convert.go new file mode 100644 index 0000000000..d326d50d35 --- /dev/null +++ b/internal/util/convert.go @@ -0,0 +1,30 @@ +package util + +import ( + "fmt" + "math" + "strconv" +) + +// ParseFloat parses a Redis RESP3 float reply into a Go float64, +// handling "inf", "-inf", "nan" per Redis conventions. +func ParseStringToFloat(s string) (float64, error) { + switch s { + case "inf": + return math.Inf(1), nil + case "-inf": + return math.Inf(-1), nil + case "nan", "-nan": + return math.NaN(), nil + } + return strconv.ParseFloat(s, 64) +} + +// MustParseFloat is like ParseFloat but panics on parse errors. +func MustParseFloat(s string) float64 { + f, err := ParseStringToFloat(s) + if err != nil { + panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err)) + } + return f +} diff --git a/internal/util/convert_test.go b/internal/util/convert_test.go new file mode 100644 index 0000000000..ffa3ee9faf --- /dev/null +++ b/internal/util/convert_test.go @@ -0,0 +1,40 @@ +package util + +import ( + "math" + "testing" +) + +func TestParseStringToFloat(t *testing.T) { + tests := []struct { + in string + want float64 + ok bool + }{ + {"1.23", 1.23, true}, + {"inf", math.Inf(1), true}, + {"-inf", math.Inf(-1), true}, + {"nan", math.NaN(), true}, + {"oops", 0, false}, + } + + for _, tc := range tests { + got, err := ParseStringToFloat(tc.in) + if tc.ok { + if err != nil { + t.Fatalf("ParseFloat(%q) error: %v", tc.in, err) + } + if math.IsNaN(tc.want) { + if !math.IsNaN(got) { + t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got) + } + } else if got != tc.want { + t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want) + } + } else { + if err == nil { + t.Errorf("ParseFloat(%q) expected error, got nil", tc.in) + } + } + } +} diff --git a/search_test.go b/search_test.go index 019acbe338..fdcd0d24b7 100644 --- a/search_test.go +++ b/search_test.go @@ -1,15 +1,18 @@ package redis_test import ( + "bytes" "context" + "encoding/binary" "fmt" - "strconv" + "math" "strings" "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/helper" ) func WaitForIndexing(c *redis.Client, index string) { @@ -27,6 +30,14 @@ func WaitForIndexing(c *redis.Client, index string) { } } +func encodeFloat32Vector(vec []float32) []byte { + buf := new(bytes.Buffer) + for _, v := range vec { + binary.Write(buf, binary.LittleEndian, v) + } + return buf.Bytes() +} + var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -693,9 +704,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res).ToNot(BeNil()) Expect(len(res.Rows)).To(BeEquivalentTo(2)) - score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64) + score1, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) - score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64) + score2, err := helper.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) Expect(score1).To(BeNumerically(">", score2)) @@ -712,9 +723,9 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(resDM).ToNot(BeNil()) Expect(len(resDM.Rows)).To(BeEquivalentTo(2)) - score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64) + score1DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) - score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64) + score2DM, err := helper.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"])) Expect(err).NotTo(HaveOccurred()) Expect(score1DM).To(BeNumerically(">", score2DM)) @@ -1684,6 +1695,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) }) + It("should return special float scores in FT.SEARCH vecsim", Label("search", "ftsearch", "vecsim"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") + + vecField := &redis.FTFlatOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "IP", + } + _, err := client.FTCreate(ctx, "idx_vec", + &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"doc:"}}, + &redis.FieldSchema{FieldName: "vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{FlatOptions: vecField}}).Result() + Expect(err).NotTo(HaveOccurred()) + WaitForIndexing(client, "idx_vec") + + bigPos := []float32{1e38, 1e38} + bigNeg := []float32{-1e38, -1e38} + nanVec := []float32{float32(math.NaN()), 0} + negNanVec := []float32{float32(math.Copysign(math.NaN(), -1)), 0} + + client.HSet(ctx, "doc:1", "vector", encodeFloat32Vector(bigPos)) + client.HSet(ctx, "doc:2", "vector", encodeFloat32Vector(bigNeg)) + client.HSet(ctx, "doc:3", "vector", encodeFloat32Vector(nanVec)) + client.HSet(ctx, "doc:4", "vector", encodeFloat32Vector(negNanVec)) + + searchOptions := &redis.FTSearchOptions{WithScores: true, Params: map[string]interface{}{"vec": encodeFloat32Vector(bigPos)}} + res, err := client.FTSearchWithArgs(ctx, "idx_vec", "*=>[KNN 4 @vector $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(4)) + + var scores []float64 + for _, row := range res.Docs { + raw := fmt.Sprintf("%v", row.Fields["__vector_score"]) + f, err := helper.ParseFloat(raw) + Expect(err).NotTo(HaveOccurred()) + scores = append(scores, f) + } + + Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(1)))) + Expect(scores).To(ContainElement(BeNumerically("==", math.Inf(-1)))) + + // For NaN values, use a custom check since NaN != NaN in floating point math + nanCount := 0 + for _, score := range scores { + if math.IsNaN(score) { + nanCount++ + } + } + Expect(nanCount).To(Equal(2)) + }) + It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() { SkipBeforeRedisVersion(7.9, "requires Redis 8.x") val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{ From a2fc5599cab2101b82707b43d9d4fb5e95f7f121 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Thu, 15 May 2025 19:53:40 +0800 Subject: [PATCH 170/230] feat: add ParseFailoverURL (#3362) * add ParseFailoverURL for FailoverOptions * fix 2 warning Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- example_test.go | 4 +- sentinel.go | 142 +++++++++++++++++++++++++ sentinel_test.go | 267 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 411 insertions(+), 2 deletions(-) diff --git a/example_test.go b/example_test.go index 28d14b65aa..c20e8390ad 100644 --- a/example_test.go +++ b/example_test.go @@ -359,7 +359,7 @@ func ExampleMapStringStringCmd_Scan() { // Get the map. The same approach works for HmGet(). res := rdb.HGetAll(ctx, "map") if res.Err() != nil { - panic(err) + panic(res.Err()) } type data struct { @@ -392,7 +392,7 @@ func ExampleSliceCmd_Scan() { res := rdb.MGet(ctx, "name", "count", "empty", "correct") if res.Err() != nil { - panic(err) + panic(res.Err()) } type data struct { diff --git a/sentinel.go b/sentinel.go index cfc848cf0c..314bde1ef6 100644 --- a/sentinel.go +++ b/sentinel.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "net" + "net/url" + "strconv" "strings" "sync" "time" @@ -220,6 +222,146 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { } } +// ParseFailoverURL parses a URL into FailoverOptions that can be used to connect to Redis. +// The URL must be in the form: +// +// redis://:@:/ +// or +// rediss://:@:/ +// +// To add additional addresses, specify the query parameter, "addr" one or more times. e.g: +// +// redis://:@:/?addr=:&addr=: +// or +// rediss://:@:/?addr=:&addr=: +// +// Most Option fields can be set using query parameters, with the following restrictions: +// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries +// - only scalar type fields are supported (bool, int, time.Duration) +// - for time.Duration fields, values must be a valid input for time.ParseDuration(); +// additionally a plain integer as value (i.e. without unit) is interpreted as seconds +// - to disable a duration field, use value less than or equal to 0; to use the default +// value, leave the value blank or remove the parameter +// - only the last value is interpreted if a parameter is given multiple times +// - fields "network", "addr", "sentinel_username" and "sentinel_password" can only be set using other +// URL attributes (scheme, host, userinfo, resp.), query parameters using these +// names will be treated as unknown parameters +// - unknown parameter names will result in an error +// +// Example: +// +// redis://user:password@localhost:6789?master_name=mymaster&dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791 +// is equivalent to: +// &FailoverOptions{ +// MasterName: "mymaster", +// Addr: ["localhost:6789", "localhost:6790", "localhost:6791"] +// DialTimeout: 3 * time.Second, // no time unit = seconds +// ReadTimeout: 6 * time.Second, +// } +func ParseFailoverURL(redisURL string) (*FailoverOptions, error) { + u, err := url.Parse(redisURL) + if err != nil { + return nil, err + } + return setupFailoverConn(u) +} + +func setupFailoverConn(u *url.URL) (*FailoverOptions, error) { + o := &FailoverOptions{} + + o.SentinelUsername, o.SentinelPassword = getUserPassword(u) + + h, p := getHostPortWithDefaults(u) + o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p)) + + switch u.Scheme { + case "rediss": + o.TLSConfig = &tls.Config{ServerName: h, MinVersion: tls.VersionTLS12} + case "redis": + o.TLSConfig = nil + default: + return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme) + } + + f := strings.FieldsFunc(u.Path, func(r rune) bool { + return r == '/' + }) + switch len(f) { + case 0: + o.DB = 0 + case 1: + var err error + if o.DB, err = strconv.Atoi(f[0]); err != nil { + return nil, fmt.Errorf("redis: invalid database number: %q", f[0]) + } + default: + return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path) + } + + return setupFailoverConnParams(u, o) +} + +func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, error) { + q := queryOptions{q: u.Query()} + + o.MasterName = q.string("master_name") + o.ClientName = q.string("client_name") + o.RouteByLatency = q.bool("route_by_latency") + o.RouteRandomly = q.bool("route_randomly") + o.ReplicaOnly = q.bool("replica_only") + o.UseDisconnectedReplicas = q.bool("use_disconnected_replicas") + o.Protocol = q.int("protocol") + o.Username = q.string("username") + o.Password = q.string("password") + o.MaxRetries = q.int("max_retries") + o.MinRetryBackoff = q.duration("min_retry_backoff") + o.MaxRetryBackoff = q.duration("max_retry_backoff") + o.DialTimeout = q.duration("dial_timeout") + o.ReadTimeout = q.duration("read_timeout") + o.WriteTimeout = q.duration("write_timeout") + o.ContextTimeoutEnabled = q.bool("context_timeout_enabled") + o.PoolFIFO = q.bool("pool_fifo") + o.PoolSize = q.int("pool_size") + o.MinIdleConns = q.int("min_idle_conns") + o.MaxIdleConns = q.int("max_idle_conns") + o.MaxActiveConns = q.int("max_active_conns") + o.ConnMaxLifetime = q.duration("conn_max_lifetime") + o.ConnMaxIdleTime = q.duration("conn_max_idle_time") + o.PoolTimeout = q.duration("pool_timeout") + o.DisableIdentity = q.bool("disableIdentity") + o.IdentitySuffix = q.string("identitySuffix") + o.UnstableResp3 = q.bool("unstable_resp3") + + if q.err != nil { + return nil, q.err + } + + if tmp := q.string("db"); tmp != "" { + db, err := strconv.Atoi(tmp) + if err != nil { + return nil, fmt.Errorf("redis: invalid database number: %w", err) + } + o.DB = db + } + + addrs := q.strings("addr") + for _, addr := range addrs { + h, p, err := net.SplitHostPort(addr) + if err != nil || h == "" || p == "" { + return nil, fmt.Errorf("redis: unable to parse addr param: %s", addr) + } + + o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p)) + } + + // any parameters left? + if r := q.remaining(); len(r) > 0 { + return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", ")) + } + + return o, nil +} + // NewFailoverClient returns a Redis client that uses Redis Sentinel // for automatic failover. It's safe for concurrent use by multiple // goroutines. diff --git a/sentinel_test.go b/sentinel_test.go index 2d481d5fc3..436895ff2f 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -2,7 +2,11 @@ package redis_test import ( "context" + "crypto/tls" + "errors" "net" + "sort" + "testing" "time" . "github.com/bsm/ginkgo/v2" @@ -405,3 +409,266 @@ var _ = Describe("SentinelAclAuth", func() { Expect(val).To(Equal("acl-auth")) }) }) + +func TestParseFailoverURL(t *testing.T) { + cases := []struct { + url string + o *redis.FailoverOptions + err error + }{ + { + url: "redis://localhost:6379?master_name=test", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test"}, + }, + { + url: "redis://localhost:6379/5?master_name=test", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test", DB: 5}, + }, + { + url: "rediss://localhost:6379/5?master_name=test", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test", DB: 5, + TLSConfig: &tls.Config{ + ServerName: "localhost", + }}, + }, + { + url: "redis://localhost:6379/5?master_name=test&db=2", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test", DB: 2}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&addr=localhost:6381", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379", "localhost:6381"}, DB: 5}, + }, + { + url: "redis://foo:bar@localhost:6379/5?addr=localhost:6380", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "bar", DB: 5}, + }, + { + url: "redis://:bar@localhost:6379/5?addr=localhost:6380", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "", SentinelPassword: "bar", DB: 5}, + }, + { + url: "redis://foo@localhost:6379/5?addr=localhost:6380", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "", DB: 5}, + }, + { + url: "redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "bar", DB: 5, DialTimeout: 3 * time.Second}, + }, + { + url: "redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3s", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "bar", DB: 5, DialTimeout: 3 * time.Second}, + }, + { + url: "redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3ms", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "bar", DB: 5, DialTimeout: 3 * time.Millisecond}, + }, + { + url: "redis://foo:bar@localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo=true", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + SentinelUsername: "foo", SentinelPassword: "bar", DB: 5, DialTimeout: 3 * time.Second, PoolFIFO: true}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo=false", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: 3 * time.Second, PoolFIFO: false}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=3&pool_fifo", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: 3 * time.Second, PoolFIFO: false}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: 0}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=0", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: -1}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=-1", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: -1}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=-2", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: -1}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: 0}, + }, + { + url: "redis://localhost:6379/5?addr=localhost:6380&dial_timeout=0&abc=5", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6380", "localhost:6379"}, + DB: 5, DialTimeout: -1}, + err: errors.New("redis: unexpected option: abc"), + }, + { + url: "http://google.com", + err: errors.New("redis: invalid URL scheme: http"), + }, + { + url: "redis://localhost/1/2/3/4", + err: errors.New("redis: invalid URL path: /1/2/3/4"), + }, + { + url: "12345", + err: errors.New("redis: invalid URL scheme: "), + }, + { + url: "redis://localhost/database", + err: errors.New(`redis: invalid database number: "database"`), + }, + } + + for i := range cases { + tc := cases[i] + t.Run(tc.url, func(t *testing.T) { + t.Parallel() + + actual, err := redis.ParseFailoverURL(tc.url) + if tc.err == nil && err != nil { + t.Fatalf("unexpected error: %q", err) + return + } + if tc.err != nil && err == nil { + t.Fatalf("got nil, expected %q", tc.err) + return + } + if tc.err != nil && err != nil { + if tc.err.Error() != err.Error() { + t.Fatalf("got %q, expected %q", err, tc.err) + } + return + } + compareFailoverOptions(t, actual, tc.o) + }) + } +} + +func compareFailoverOptions(t *testing.T, a, e *redis.FailoverOptions) { + if a.MasterName != e.MasterName { + t.Errorf("MasterName got %q, want %q", a.MasterName, e.MasterName) + } + compareSlices(t, a.SentinelAddrs, e.SentinelAddrs, "SentinelAddrs") + if a.ClientName != e.ClientName { + t.Errorf("ClientName got %q, want %q", a.ClientName, e.ClientName) + } + if a.SentinelUsername != e.SentinelUsername { + t.Errorf("SentinelUsername got %q, want %q", a.SentinelUsername, e.SentinelUsername) + } + if a.SentinelPassword != e.SentinelPassword { + t.Errorf("SentinelPassword got %q, want %q", a.SentinelPassword, e.SentinelPassword) + } + if a.RouteByLatency != e.RouteByLatency { + t.Errorf("RouteByLatency got %v, want %v", a.RouteByLatency, e.RouteByLatency) + } + if a.RouteRandomly != e.RouteRandomly { + t.Errorf("RouteRandomly got %v, want %v", a.RouteRandomly, e.RouteRandomly) + } + if a.ReplicaOnly != e.ReplicaOnly { + t.Errorf("ReplicaOnly got %v, want %v", a.ReplicaOnly, e.ReplicaOnly) + } + if a.UseDisconnectedReplicas != e.UseDisconnectedReplicas { + t.Errorf("UseDisconnectedReplicas got %v, want %v", a.UseDisconnectedReplicas, e.UseDisconnectedReplicas) + } + if a.Protocol != e.Protocol { + t.Errorf("Protocol got %v, want %v", a.Protocol, e.Protocol) + } + if a.Username != e.Username { + t.Errorf("Username got %q, want %q", a.Username, e.Username) + } + if a.Password != e.Password { + t.Errorf("Password got %q, want %q", a.Password, e.Password) + } + if a.DB != e.DB { + t.Errorf("DB got %v, want %v", a.DB, e.DB) + } + if a.MaxRetries != e.MaxRetries { + t.Errorf("MaxRetries got %v, want %v", a.MaxRetries, e.MaxRetries) + } + if a.MinRetryBackoff != e.MinRetryBackoff { + t.Errorf("MinRetryBackoff got %v, want %v", a.MinRetryBackoff, e.MinRetryBackoff) + } + if a.MaxRetryBackoff != e.MaxRetryBackoff { + t.Errorf("MaxRetryBackoff got %v, want %v", a.MaxRetryBackoff, e.MaxRetryBackoff) + } + if a.DialTimeout != e.DialTimeout { + t.Errorf("DialTimeout got %v, want %v", a.DialTimeout, e.DialTimeout) + } + if a.ReadTimeout != e.ReadTimeout { + t.Errorf("ReadTimeout got %v, want %v", a.ReadTimeout, e.ReadTimeout) + } + if a.WriteTimeout != e.WriteTimeout { + t.Errorf("WriteTimeout got %v, want %v", a.WriteTimeout, e.WriteTimeout) + } + if a.ContextTimeoutEnabled != e.ContextTimeoutEnabled { + t.Errorf("ContentTimeoutEnabled got %v, want %v", a.ContextTimeoutEnabled, e.ContextTimeoutEnabled) + } + if a.PoolFIFO != e.PoolFIFO { + t.Errorf("PoolFIFO got %v, want %v", a.PoolFIFO, e.PoolFIFO) + } + if a.PoolSize != e.PoolSize { + t.Errorf("PoolSize got %v, want %v", a.PoolSize, e.PoolSize) + } + if a.PoolTimeout != e.PoolTimeout { + t.Errorf("PoolTimeout got %v, want %v", a.PoolTimeout, e.PoolTimeout) + } + if a.MinIdleConns != e.MinIdleConns { + t.Errorf("MinIdleConns got %v, want %v", a.MinIdleConns, e.MinIdleConns) + } + if a.MaxIdleConns != e.MaxIdleConns { + t.Errorf("MaxIdleConns got %v, want %v", a.MaxIdleConns, e.MaxIdleConns) + } + if a.MaxActiveConns != e.MaxActiveConns { + t.Errorf("MaxActiveConns got %v, want %v", a.MaxActiveConns, e.MaxActiveConns) + } + if a.ConnMaxIdleTime != e.ConnMaxIdleTime { + t.Errorf("ConnMaxIdleTime got %v, want %v", a.ConnMaxIdleTime, e.ConnMaxIdleTime) + } + if a.ConnMaxLifetime != e.ConnMaxLifetime { + t.Errorf("ConnMaxLifeTime got %v, want %v", a.ConnMaxLifetime, e.ConnMaxLifetime) + } + if a.DisableIdentity != e.DisableIdentity { + t.Errorf("DisableIdentity got %v, want %v", a.DisableIdentity, e.DisableIdentity) + } + if a.IdentitySuffix != e.IdentitySuffix { + t.Errorf("IdentitySuffix got %v, want %v", a.IdentitySuffix, e.IdentitySuffix) + } + if a.UnstableResp3 != e.UnstableResp3 { + t.Errorf("UnstableResp3 got %v, want %v", a.UnstableResp3, e.UnstableResp3) + } + if (a.TLSConfig == nil && e.TLSConfig != nil) || (a.TLSConfig != nil && e.TLSConfig == nil) { + t.Errorf("TLSConfig error") + } + if a.TLSConfig != nil && e.TLSConfig != nil { + if a.TLSConfig.ServerName != e.TLSConfig.ServerName { + t.Errorf("TLSConfig.ServerName got %q, want %q", a.TLSConfig.ServerName, e.TLSConfig.ServerName) + } + } +} + +func compareSlices(t *testing.T, a, b []string, name string) { + sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) + sort.Slice(b, func(i, j int) bool { return b[i] < b[j] }) + if len(a) != len(b) { + t.Errorf("%s got %q, want %q", name, a, b) + } + for i := range a { + if a[i] != b[i] { + t.Errorf("%s got %q, want %q", name, a, b) + } + } +} From f2818c7f708c86fdbf9f9f44125dbb890f76ca18 Mon Sep 17 00:00:00 2001 From: Amir Salehi <54236454+iamamirsalehi@users.noreply.github.com> Date: Thu, 15 May 2025 15:23:54 +0330 Subject: [PATCH 171/230] test(util): add unit tests for Atoi, ParseInt, ParseUint, and ParseFloat (#3377) --- internal/util/strconv_test.go | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 internal/util/strconv_test.go diff --git a/internal/util/strconv_test.go b/internal/util/strconv_test.go new file mode 100644 index 0000000000..64586e0c58 --- /dev/null +++ b/internal/util/strconv_test.go @@ -0,0 +1,101 @@ +package util + +import ( + "math" + "testing" +) + +func TestAtoi(t *testing.T) { + tests := []struct { + input []byte + expected int + wantErr bool + }{ + {[]byte("123"), 123, false}, + {[]byte("-456"), -456, false}, + {[]byte("abc"), 0, true}, + } + + for _, tt := range tests { + result, err := Atoi(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("Atoi(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + if result != tt.expected && !tt.wantErr { + t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected) + } + } +} + +func TestParseInt(t *testing.T) { + tests := []struct { + input []byte + base int + bitSize int + expected int64 + wantErr bool + }{ + {[]byte("123"), 10, 64, 123, false}, + {[]byte("-7F"), 16, 64, -127, false}, + {[]byte("zzz"), 36, 64, 46655, false}, + {[]byte("invalid"), 10, 64, 0, true}, + } + + for _, tt := range tests { + result, err := ParseInt(tt.input, tt.base, tt.bitSize) + if (err != nil) != tt.wantErr { + t.Errorf("ParseInt(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) + } + if result != tt.expected && !tt.wantErr { + t.Errorf("ParseInt(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) + } + } +} + +func TestParseUint(t *testing.T) { + tests := []struct { + input []byte + base int + bitSize int + expected uint64 + wantErr bool + }{ + {[]byte("255"), 10, 8, 255, false}, + {[]byte("FF"), 16, 16, 255, false}, + {[]byte("-1"), 10, 8, 0, true}, // negative should error for unsigned + } + + for _, tt := range tests { + result, err := ParseUint(tt.input, tt.base, tt.bitSize) + if (err != nil) != tt.wantErr { + t.Errorf("ParseUint(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) + } + if result != tt.expected && !tt.wantErr { + t.Errorf("ParseUint(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) + } + } +} + +func TestParseFloat(t *testing.T) { + tests := []struct { + input []byte + bitSize int + expected float64 + wantErr bool + }{ + {[]byte("3.14"), 64, 3.14, false}, + {[]byte("-2.71"), 64, -2.71, false}, + {[]byte("NaN"), 64, math.NaN(), false}, + {[]byte("invalid"), 64, 0, true}, + } + + for _, tt := range tests { + result, err := ParseFloat(tt.input, tt.bitSize) + if (err != nil) != tt.wantErr { + t.Errorf("ParseFloat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + if !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected { + t.Errorf("ParseFloat(%q) = %v, want %v", tt.input, result, tt.expected) + } + } +} From 7265b22519977ff41eae8cc89947ac59fc029166 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 20 May 2025 00:21:17 +0800 Subject: [PATCH 172/230] chore: optimize function `ReplaceSpaces` (#3383) * chore: optimize function `ReplaceSpaces` Signed-off-by: fukua95 * trigger CI again because the bug of docker Signed-off-by: fukua95 * trigger CI again because the bug of docker Signed-off-by: fukua95 --------- Signed-off-by: fukua95 --- internal/util.go | 17 +---------------- internal/util_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/internal/util.go b/internal/util.go index cc1bff24e6..f77775ff40 100644 --- a/internal/util.go +++ b/internal/util.go @@ -49,22 +49,7 @@ func isLower(s string) bool { } func ReplaceSpaces(s string) string { - // Pre-allocate a builder with the same length as s to minimize allocations. - // This is a basic optimization; adjust the initial size based on your use case. - var builder strings.Builder - builder.Grow(len(s)) - - for _, char := range s { - if char == ' ' { - // Replace space with a hyphen. - builder.WriteRune('-') - } else { - // Copy the character as-is. - builder.WriteRune(char) - } - } - - return builder.String() + return strings.ReplaceAll(s, " ", "-") } func GetAddr(addr string) string { diff --git a/internal/util_test.go b/internal/util_test.go index 57f7f9fa15..0bc4664667 100644 --- a/internal/util_test.go +++ b/internal/util_test.go @@ -1,6 +1,7 @@ package internal import ( + "runtime" "strings" "testing" @@ -72,3 +73,36 @@ func TestGetAddr(t *testing.T) { Expect(GetAddr("127")).To(Equal("")) }) } + +func BenchmarkReplaceSpaces(b *testing.B) { + version := runtime.Version() + for i := 0; i < b.N; i++ { + _ = ReplaceSpaces(version) + } +} + +func ReplaceSpacesUseBuilder(s string) string { + // Pre-allocate a builder with the same length as s to minimize allocations. + // This is a basic optimization; adjust the initial size based on your use case. + var builder strings.Builder + builder.Grow(len(s)) + + for _, char := range s { + if char == ' ' { + // Replace space with a hyphen. + builder.WriteRune('-') + } else { + // Copy the character as-is. + builder.WriteRune(char) + } + } + + return builder.String() +} + +func BenchmarkReplaceSpacesUseBuilder(b *testing.B) { + version := runtime.Version() + for i := 0; i < b.N; i++ { + _ = ReplaceSpacesUseBuilder(version) + } +} From 81bd24deb0498542c26c61b331b484a19d133a48 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 20 May 2025 00:22:51 +0800 Subject: [PATCH 173/230] chore: remove unused param (#3382) * chore: remove unused param Signed-off-by: fukua95 * chore: rename a unused param to `_` Signed-off-by: fukua95 --------- Signed-off-by: fukua95 --- internal_test.go | 4 ++-- osscluster.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal_test.go b/internal_test.go index 516ada8236..bd7d237a86 100644 --- a/internal_test.go +++ b/internal_test.go @@ -364,14 +364,14 @@ var _ = Describe("ClusterClient", func() { It("select slot from args for GETKEYSINSLOT command", func() { cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200) - slot := client.cmdSlot(context.Background(), cmd) + slot := client.cmdSlot(cmd) Expect(slot).To(Equal(100)) }) It("select slot from args for COUNTKEYSINSLOT command", func() { cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100) - slot := client.cmdSlot(context.Background(), cmd) + slot := client.cmdSlot(cmd) Expect(slot).To(Equal(100)) }) }) diff --git a/osscluster.go b/osscluster.go index c0278ed057..39871b21fd 100644 --- a/osscluster.go +++ b/osscluster.go @@ -981,7 +981,7 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { } func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { - slot := c.cmdSlot(ctx, cmd) + slot := c.cmdSlot(cmd) var node *clusterNode var moved bool var ask bool @@ -1329,7 +1329,7 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) { for _, cmd := range cmds { - slot := c.cmdSlot(ctx, cmd) + slot := c.cmdSlot(cmd) node, err := c.slotReadOnlyNode(state, slot) if err != nil { return err @@ -1340,7 +1340,7 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd } for _, cmd := range cmds { - slot := c.cmdSlot(ctx, cmd) + slot := c.cmdSlot(cmd) node, err := state.slotMasterNode(slot) if err != nil { return err @@ -1540,7 +1540,7 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err func (c *ClusterClient) mapCmdsBySlot(ctx context.Context, cmds []Cmder) map[int][]Cmder { cmdsMap := make(map[int][]Cmder) for _, cmd := range cmds { - slot := c.cmdSlot(ctx, cmd) + slot := c.cmdSlot(cmd) cmdsMap[slot] = append(cmdsMap[slot], cmd) } return cmdsMap @@ -1569,7 +1569,7 @@ func (c *ClusterClient) processTxPipelineNode( } func (c *ClusterClient) processTxPipelineNodeConn( - ctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, + ctx context.Context, _ *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap, ) error { if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmds(wr, cmds) @@ -1858,7 +1858,7 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { return info } -func (c *ClusterClient) cmdSlot(ctx context.Context, cmd Cmder) int { +func (c *ClusterClient) cmdSlot(cmd Cmder) int { args := cmd.Args() if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { return args[2].(int) From e25a391e9ea3172d48a66176d3d733efac1adc4c Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Mon, 19 May 2025 12:23:10 -0400 Subject: [PATCH 174/230] Export pool errors for public consumption (#3380) --- error.go | 7 +++++++ export_test.go | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/error.go b/error.go index 6f47f7cf2c..8c811966fb 100644 --- a/error.go +++ b/error.go @@ -15,6 +15,13 @@ import ( // ErrClosed performs any operation on the closed client will return this error. var ErrClosed = pool.ErrClosed +// ErrPoolExhausted is returned from a pool connection method +// when the maximum number of database connections in the pool has been reached. +var ErrPoolExhausted = pool.ErrPoolExhausted + +// ErrPoolTimeout timed out waiting to get a connection from the connection pool. +var ErrPoolTimeout = pool.ErrPoolTimeout + // HasErrorPrefix checks if the err is a Redis error and the message contains a prefix. func HasErrorPrefix(err error, prefix string) bool { var rErr Error diff --git a/export_test.go b/export_test.go index 10d8f23ce5..c1b77683f3 100644 --- a/export_test.go +++ b/export_test.go @@ -11,8 +11,6 @@ import ( "github.com/redis/go-redis/v9/internal/pool" ) -var ErrPoolTimeout = pool.ErrPoolTimeout - func (c *baseClient) Pool() pool.Pooler { return c.connPool } From 27c6e8b2904d92a3ddb28b3fc9716f671ba41997 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 20 May 2025 00:24:37 +0800 Subject: [PATCH 175/230] perf: avoid unnecessary copy operation (#3376) * optime: reduce unnecessary copy operations * add a comment * trigger CI without code changes, because the bug of docker * add comments --- ring.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ring.go b/ring.go index 555ea2a163..ab3d062664 100644 --- a/ring.go +++ b/ring.go @@ -349,17 +349,16 @@ func (c *ringSharding) newRingShards( return } +// Warning: External exposure of `c.shards.list` may cause data races. +// So keep internal or implement deep copy if exposed. func (c *ringSharding) List() []*ringShard { - var list []*ringShard - c.mu.RLock() - if !c.closed { - list = make([]*ringShard, len(c.shards.list)) - copy(list, c.shards.list) - } - c.mu.RUnlock() + defer c.mu.RUnlock() - return list + if c.closed { + return nil + } + return c.shards.list } func (c *ringSharding) Hash(key string) string { @@ -423,6 +422,7 @@ func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) { case <-ticker.C: var rebalance bool + // note: `c.List()` return a shadow copy of `[]*ringShard`. for _, shard := range c.List() { err := shard.Client.Ping(ctx).Err() isUp := err == nil || err == pool.ErrPoolTimeout @@ -582,6 +582,7 @@ func (c *Ring) retryBackoff(attempt int) time.Duration { // PoolStats returns accumulated connection pool stats. func (c *Ring) PoolStats() *PoolStats { + // note: `c.List()` return a shadow copy of `[]*ringShard`. shards := c.sharding.List() var acc PoolStats for _, shard := range shards { @@ -651,6 +652,7 @@ func (c *Ring) ForEachShard( ctx context.Context, fn func(ctx context.Context, client *Client) error, ) error { + // note: `c.List()` return a shadow copy of `[]*ringShard`. shards := c.sharding.List() var wg sync.WaitGroup errCh := make(chan error, 1) @@ -682,6 +684,7 @@ func (c *Ring) ForEachShard( } func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) { + // note: `c.List()` return a shadow copy of `[]*ringShard`. shards := c.sharding.List() var firstErr error for _, shard := range shards { @@ -810,7 +813,7 @@ func (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) er for _, key := range keys { if key != "" { - shard, err := c.sharding.GetByKey(hashtag.Key(key)) + shard, err := c.sharding.GetByKey(key) if err != nil { return err } From 46da738279718b0b0a5ed7813082951c9fc2a4ce Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Mon, 19 May 2025 12:46:19 -0400 Subject: [PATCH 176/230] Unit test for pool acquisition timeout (#3381) Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- internal/pool/pool_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index d198ba5400..0f366cc7de 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -387,4 +387,33 @@ var _ = Describe("race", func() { Expect(stats.WaitCount).To(Equal(uint32(1))) Expect(stats.WaitDurationNs).To(BeNumerically("~", time.Second.Nanoseconds(), 100*time.Millisecond.Nanoseconds())) }) + + It("timeout", func() { + testPoolTimeout := 1 * time.Second + opt := &pool.Options{ + Dialer: func(ctx context.Context) (net.Conn, error) { + // Artificial delay to force pool timeout + time.Sleep(3 * testPoolTimeout) + + return &net.TCPConn{}, nil + }, + PoolSize: 1, + PoolTimeout: testPoolTimeout, + } + p := pool.NewConnPool(opt) + + stats := p.Stats() + Expect(stats.Timeouts).To(Equal(uint32(0))) + + conn, err := p.Get(ctx) + Expect(err).NotTo(HaveOccurred()) + _, err = p.Get(ctx) + Expect(err).To(MatchError(pool.ErrPoolTimeout)) + p.Put(ctx, conn) + conn, err = p.Get(ctx) + Expect(err).NotTo(HaveOccurred()) + + stats = p.Stats() + Expect(stats.Timeouts).To(Equal(uint32(1))) + }) }) From 368a1c7739bdef28ae78714b0e37fd791fecb77e Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 20 May 2025 19:11:53 +0800 Subject: [PATCH 177/230] chore: remove unused param (#3384) Signed-off-by: fukua95 --- osscluster.go | 4 ++-- ring.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osscluster.go b/osscluster.go index 39871b21fd..2365f771d9 100644 --- a/osscluster.go +++ b/osscluster.go @@ -1498,7 +1498,7 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err return err } - cmdsMap := c.mapCmdsBySlot(ctx, cmds) + cmdsMap := c.mapCmdsBySlot(cmds) for slot, cmds := range cmdsMap { node, err := state.slotMasterNode(slot) if err != nil { @@ -1537,7 +1537,7 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err return cmdsFirstErr(cmds) } -func (c *ClusterClient) mapCmdsBySlot(ctx context.Context, cmds []Cmder) map[int][]Cmder { +func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder { cmdsMap := make(map[int][]Cmder) for _, cmd := range cmds { slot := c.cmdSlot(cmd) diff --git a/ring.go b/ring.go index ab3d062664..fe8a6dc47b 100644 --- a/ring.go +++ b/ring.go @@ -702,7 +702,7 @@ func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) { return nil, firstErr } -func (c *Ring) cmdShard(ctx context.Context, cmd Cmder) (*ringShard, error) { +func (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) { pos := cmdFirstKeyPos(cmd) if pos == 0 { return c.sharding.Random() @@ -720,7 +720,7 @@ func (c *Ring) process(ctx context.Context, cmd Cmder) error { } } - shard, err := c.cmdShard(ctx, cmd) + shard, err := c.cmdShard(cmd) if err != nil { return err } From 27ab6420910de929be25661870c0206444eaa2e7 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 21 May 2025 13:57:58 +0300 Subject: [PATCH 178/230] xinfo-groups: support nil lag in XINFO GROUPS (#3369) * xinfo-groups: support nil lag in XINFO GROUPS * Add test * docs: clarify XInfoGroup.Lag field behavior with Nil values * docs: clarify XInfoGroup.Lag field behavior --- command.go | 7 ++++++- commands_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 3253af6cc9..5fa347f43f 100644 --- a/command.go +++ b/command.go @@ -2104,7 +2104,9 @@ type XInfoGroup struct { Pending int64 LastDeliveredID string EntriesRead int64 - Lag int64 + // Lag represents the number of pending messages in the stream not yet + // delivered to this consumer group. Returns -1 when the lag cannot be determined. + Lag int64 } var _ Cmder = (*XInfoGroupsCmd)(nil) @@ -2187,8 +2189,11 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { // lag: the number of entries in the stream that are still waiting to be delivered // to the group's consumers, or a NULL(Nil) when that number can't be determined. + // In that case, we return -1. if err != nil && err != Nil { return err + } else if err == Nil { + group.Lag = -1 } default: return fmt.Errorf("redis: unexpected key %q in XINFO GROUPS reply", key) diff --git a/commands_test.go b/commands_test.go index 8b2aa37d47..5256a6fbfa 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6772,6 +6772,36 @@ var _ = Describe("Commands", func() { })) }) + It("should return -1 for nil lag in XINFO GROUPS", func() { + _, err := client.XAdd(ctx, &redis.XAddArgs{Stream: "s", ID: "0-1", Values: []string{"foo", "1"}}).Result() + Expect(err).NotTo(HaveOccurred()) + + client.XAdd(ctx, &redis.XAddArgs{Stream: "s", ID: "0-2", Values: []string{"foo", "2"}}) + Expect(err).NotTo(HaveOccurred()) + client.XAdd(ctx, &redis.XAddArgs{Stream: "s", ID: "0-3", Values: []string{"foo", "3"}}) + Expect(err).NotTo(HaveOccurred()) + + err = client.XGroupCreate(ctx, "s", "g", "0").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.XReadGroup(ctx, &redis.XReadGroupArgs{Group: "g", Consumer: "c", Streams: []string{"s", ">"}, Count: 1, Block: -1, NoAck: false}).Err() + Expect(err).NotTo(HaveOccurred()) + + client.XDel(ctx, "s", "0-2") + + res, err := client.XInfoGroups(ctx, "s").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]redis.XInfoGroup{ + { + Name: "g", + Consumers: 1, + Pending: 1, + LastDeliveredID: "0-1", + EntriesRead: 1, + Lag: -1, // nil lag from Redis is reported as -1 + }, + })) + }) + It("should XINFO CONSUMERS", func() { res, err := client.XInfoConsumers(ctx, "stream", "group1").Result() Expect(err).NotTo(HaveOccurred()) From 742335126d5160ab7b10a3fd9c7600a5d874a1b9 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 21 May 2025 13:58:21 +0300 Subject: [PATCH 179/230] fix: prevent routing reads to loading slave nodes (#3370) Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- osscluster.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osscluster.go b/osscluster.go index 2365f771d9..cc2444fa9f 100644 --- a/osscluster.go +++ b/osscluster.go @@ -445,6 +445,14 @@ func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) { } } +func (n *clusterNode) Loading() bool { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + err := n.Client.Ping(ctx).Err() + return err != nil && isLoadingError(err) +} + //------------------------------------------------------------------------------ type clusterNodes struct { @@ -754,7 +762,8 @@ func (c *clusterState) slotSlaveNode(slot int) (*clusterNode, error) { case 1: return nodes[0], nil case 2: - if slave := nodes[1]; !slave.Failing() { + slave := nodes[1] + if !slave.Failing() && !slave.Loading() { return slave, nil } return nodes[0], nil @@ -763,7 +772,7 @@ func (c *clusterState) slotSlaveNode(slot int) (*clusterNode, error) { for i := 0; i < 10; i++ { n := rand.Intn(len(nodes)-1) + 1 slave = nodes[n] - if !slave.Failing() { + if !slave.Failing() && !slave.Loading() { return slave, nil } } From 6e822c0b402069899426d6728b9735795fd5b46e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 14:52:52 +0300 Subject: [PATCH 180/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3389) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.48.0 to 0.49.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.48.0...0.49.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 4d0fc338d6..6ab2c46701 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.48.0 + uses: rojopolis/spellcheck-github-actions@0.49.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 062685de5ceef9a9ecfcdea2af0222fb6cece979 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 27 May 2025 19:53:41 +0800 Subject: [PATCH 181/230] chore: set the default value for the `options.protocol` in the `init()` of `options` (#3387) * chore: set the default value for the `options.protocol` in the `init()` of `options` Signed-off-by: fukua95 * add a test Signed-off-by: fukua95 --------- Signed-off-by: fukua95 --- options.go | 3 +++ options_test.go | 23 +++++++++++++++++++++++ redis.go | 8 +------- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/options.go b/options.go index 3ffcd07ede..bf6c032ec8 100644 --- a/options.go +++ b/options.go @@ -178,6 +178,9 @@ func (opt *Options) init() { opt.Network = "tcp" } } + if opt.Protocol < 2 { + opt.Protocol = 3 + } if opt.DialTimeout == 0 { opt.DialTimeout = 5 * time.Second } diff --git a/options_test.go b/options_test.go index d46ecc8583..8de4986b3c 100644 --- a/options_test.go +++ b/options_test.go @@ -222,3 +222,26 @@ func TestReadTimeoutOptions(t *testing.T) { } } } + +func TestProtocolOptions(t *testing.T) { + testCasesMap := map[int]int{ + 0: 3, + 1: 3, + 2: 2, + 3: 3, + } + + o := &Options{} + o.init() + if o.Protocol != 3 { + t.Errorf("got %d instead of %d as protocol option", o.Protocol, 3) + } + + for set, want := range testCasesMap { + o := &Options{Protocol: set} + o.init() + if o.Protocol != want { + t.Errorf("got %d instead of %d as protocol option", o.Protocol, want) + } + } +} diff --git a/redis.go b/redis.go index f50df5689b..19ff57b017 100644 --- a/redis.go +++ b/redis.go @@ -302,15 +302,9 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { conn := newConn(c.opt, connPool) var auth bool - protocol := c.opt.Protocol - // By default, use RESP3 in current version. - if protocol < 2 { - protocol = 3 - } - // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, protocol, username, password, c.opt.ClientName).Err(); err == nil { + if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { auth = true } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal From 1e895f6fbe6e151b90a7868069491e7a75983206 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 27 May 2025 16:25:20 +0300 Subject: [PATCH 182/230] feat: Introducing StreamingCredentialsProvider for token based authentication (#3320) * wip * update documentation * add streamingcredentialsprovider in options * fix: put back option in pool creation * add package level comment * Initial re authentication implementation Introduces the StreamingCredentialsProvider as the CredentialsProvider with the highest priority. TODO: needs to be tested * Change function type name Change CancelProviderFunc to UnsubscribeFunc * add tests * fix race in tests * fix example tests * wip, hooks refactor * fix build * update README.md * update wordlist * update README.md * refactor(auth): early returns in cred listener * fix(doctest): simulate some delay * feat(conn): add close hook on conn * fix(tests): simulate start/stop in mock credentials provider * fix(auth): don't double close the conn * docs(README): mark streaming credentials provider as experimental * fix(auth): streamline auth err proccess * fix(auth): check err on close conn * chore(entraid): use the repo under redis org --- .github/wordlist.txt | 10 +- .gitignore | 3 +- README.md | 121 ++++++++++- auth/auth.go | 61 ++++++ auth/auth_test.go | 308 ++++++++++++++++++++++++++++ auth/reauth_credentials_listener.go | 47 +++++ command_recorder_test.go | 86 ++++++++ doctests/lpush_lrange_test.go | 2 + example_instrumentation_test.go | 53 +++-- internal/pool/conn.go | 10 + internal_test.go | 8 +- options.go | 129 ++++++++---- osscluster.go | 23 ++- osscluster_test.go | 3 + probabilistic_test.go | 6 +- redis.go | 152 +++++++++++--- redis_test.go | 169 +++++++++++++++ ring_test.go | 32 ++- sentinel.go | 3 +- tx.go | 7 +- 20 files changed, 1103 insertions(+), 130 deletions(-) create mode 100644 auth/auth.go create mode 100644 auth/auth_test.go create mode 100644 auth/reauth_credentials_listener.go create mode 100644 command_recorder_test.go diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 578616b9d0..a922d99bac 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -65,4 +65,12 @@ RedisGears RedisTimeseries RediSearch RawResult -RawVal \ No newline at end of file +RawVal +entra +EntraID +Entra +OAuth +Azure +StreamingCredentialsProvider +oauth +entraid \ No newline at end of file diff --git a/.gitignore b/.gitignore index e9c8f52641..0d99709e34 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ testdata/* redis8tests.sh coverage.txt **/coverage.txt -.vscode \ No newline at end of file +.vscode +tmp/* diff --git a/README.md b/README.md index 4487c6e9a7..c37a52ec70 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ key value NoSQL database that uses RocksDB as storage engine and is compatible w - Redis commands except QUIT and SYNC. - Automatic connection pooling. +- [StreamingCredentialsProvider (e.g. entra id, oauth)](#1-streaming-credentials-provider-highest-priority) (experimental) - [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html). - [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html). - [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html). @@ -136,17 +137,121 @@ func ExampleClient() { } ``` -The above can be modified to specify the version of the RESP protocol by adding the `protocol` -option to the `Options` struct: +### Authentication + +The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options: + +#### 1. Streaming Credentials Provider (Highest Priority) - Experimental feature + +The streaming credentials provider allows for dynamic credential updates during the connection lifetime. This is particularly useful for managed identity services and token-based authentication. ```go - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "", // no password set - DB: 0, // use default DB - Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3 - }) +type StreamingCredentialsProvider interface { + Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) +} + +type CredentialsListener interface { + OnNext(credentials Credentials) // Called when credentials are updated + OnError(err error) // Called when an error occurs +} + +type Credentials interface { + BasicAuth() (username string, password string) + RawCredentials() string +} +``` + +Example usage: +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + StreamingCredentialsProvider: &MyCredentialsProvider{}, +}) +``` + +**Note:** The streaming credentials provider can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) to enable Entra ID (formerly Azure AD) authentication. This allows for seamless integration with Azure's managed identity services and token-based authentication. + +Example with Entra ID: +```go +import ( + "github.com/redis/go-redis/v9" + "github.com/redis/go-redis-entraid" +) + +// Create an Entra ID credentials provider +provider := entraid.NewDefaultAzureIdentityProvider() + +// Configure Redis client with Entra ID authentication +rdb := redis.NewClient(&redis.Options{ + Addr: "your-redis-server.redis.cache.windows.net:6380", + StreamingCredentialsProvider: provider, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, +}) +``` +#### 2. Context-based Credentials Provider + +The context-based provider allows credentials to be determined at the time of each operation, using the context. + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + CredentialsProviderContext: func(ctx context.Context) (string, string, error) { + // Return username, password, and any error + return "user", "pass", nil + }, +}) +``` + +#### 3. Regular Credentials Provider + +A simple function-based provider that returns static credentials. + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + CredentialsProvider: func() (string, string) { + // Return username and password + return "user", "pass" + }, +}) +``` + +#### 4. Username/Password Fields (Lowest Priority) + +The most basic way to provide credentials is through the `Username` and `Password` fields in the options. + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Username: "user", + Password: "pass", +}) +``` + +#### Priority Order + +The client will use credentials in the following priority order: +1. Streaming Credentials Provider (if set) +2. Context-based Credentials Provider (if set) +3. Regular Credentials Provider (if set) +4. Username/Password fields (if set) + +If none of these are set, the client will attempt to connect without authentication. + +### Protocol Version + +The client supports both RESP2 and RESP3 protocols. You can specify the protocol version in the options: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3 +}) ``` ### Connecting via a redis url diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000000..1f5c802248 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,61 @@ +// Package auth package provides authentication-related interfaces and types. +// It also includes a basic implementation of credentials using username and password. +package auth + +// StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider. +// It is used to provide credentials for authentication. +// The CredentialsListener is used to receive updates when the credentials change. +type StreamingCredentialsProvider interface { + // Subscribe subscribes to the credentials provider for updates. + // It returns the current credentials, a cancel function to unsubscribe from the provider, + // and an error if any. + // TODO(ndyakov): Should we add context to the Subscribe method? + Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) +} + +// UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. +// It is used to unsubscribe from the provider when the credentials are no longer needed. +type UnsubscribeFunc func() error + +// CredentialsListener is an interface that defines the methods for a credentials listener. +// It is used to receive updates when the credentials change. +// The OnNext method is called when the credentials change. +// The OnError method is called when an error occurs while requesting the credentials. +type CredentialsListener interface { + OnNext(credentials Credentials) + OnError(err error) +} + +// Credentials is an interface that defines the methods for credentials. +// It is used to provide the credentials for authentication. +type Credentials interface { + // BasicAuth returns the username and password for basic authentication. + BasicAuth() (username string, password string) + // RawCredentials returns the raw credentials as a string. + // This can be used to extract the username and password from the raw credentials or + // additional information if present in the token. + RawCredentials() string +} + +type basicAuth struct { + username string + password string +} + +// RawCredentials returns the raw credentials as a string. +func (b *basicAuth) RawCredentials() string { + return b.username + ":" + b.password +} + +// BasicAuth returns the username and password for basic authentication. +func (b *basicAuth) BasicAuth() (username string, password string) { + return b.username, b.password +} + +// NewBasicCredentials creates a new Credentials object from the given username and password. +func NewBasicCredentials(username, password string) Credentials { + return &basicAuth{ + username: username, + password: password, + } +} diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000000..be762a8545 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,308 @@ +package auth + +import ( + "errors" + "sync" + "testing" + "time" +) + +type mockStreamingProvider struct { + credentials Credentials + err error + updates chan Credentials +} + +func newMockStreamingProvider(initialCreds Credentials) *mockStreamingProvider { + return &mockStreamingProvider{ + credentials: initialCreds, + updates: make(chan Credentials, 10), + } +} + +func (m *mockStreamingProvider) Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) { + if m.err != nil { + return nil, nil, m.err + } + + // Send initial credentials + listener.OnNext(m.credentials) + + // Start goroutine to handle updates + go func() { + for creds := range m.updates { + listener.OnNext(creds) + } + }() + + return m.credentials, func() error { + close(m.updates) + return nil + }, nil +} + +func TestStreamingCredentialsProvider(t *testing.T) { + t.Run("successful subscription", func(t *testing.T) { + initialCreds := NewBasicCredentials("user1", "pass1") + provider := newMockStreamingProvider(initialCreds) + + var receivedCreds []Credentials + var receivedErrors []error + var mu sync.Mutex + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + mu.Lock() + receivedCreds = append(receivedCreds, creds) + mu.Unlock() + return nil + }, + func(err error) { + receivedErrors = append(receivedErrors, err) + }, + ) + + creds, cancel, err := provider.Subscribe(listener) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cancel == nil { + t.Fatal("expected cancel function to be non-nil") + } + if creds != initialCreds { + t.Fatalf("expected credentials %v, got %v", initialCreds, creds) + } + if len(receivedCreds) != 1 { + t.Fatalf("expected 1 received credential, got %d", len(receivedCreds)) + } + if receivedCreds[0] != initialCreds { + t.Fatalf("expected received credential %v, got %v", initialCreds, receivedCreds[0]) + } + if len(receivedErrors) != 0 { + t.Fatalf("expected no errors, got %d", len(receivedErrors)) + } + + // Send an update + newCreds := NewBasicCredentials("user2", "pass2") + provider.updates <- newCreds + + // Wait for update to be processed + time.Sleep(100 * time.Millisecond) + mu.Lock() + if len(receivedCreds) != 2 { + t.Fatalf("expected 2 received credentials, got %d", len(receivedCreds)) + } + if receivedCreds[1] != newCreds { + t.Fatalf("expected received credential %v, got %v", newCreds, receivedCreds[1]) + } + mu.Unlock() + + // Cancel subscription + if err := cancel(); err != nil { + t.Fatalf("unexpected error cancelling subscription: %v", err) + } + }) + + t.Run("subscription error", func(t *testing.T) { + provider := &mockStreamingProvider{ + err: errors.New("subscription failed"), + } + + var receivedCreds []Credentials + var receivedErrors []error + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + receivedCreds = append(receivedCreds, creds) + return nil + }, + func(err error) { + receivedErrors = append(receivedErrors, err) + }, + ) + + creds, cancel, err := provider.Subscribe(listener) + if err == nil { + t.Fatal("expected error, got nil") + } + if cancel != nil { + t.Fatal("expected cancel function to be nil") + } + if creds != nil { + t.Fatalf("expected nil credentials, got %v", creds) + } + if len(receivedCreds) != 0 { + t.Fatalf("expected no received credentials, got %d", len(receivedCreds)) + } + if len(receivedErrors) != 0 { + t.Fatalf("expected no errors, got %d", len(receivedErrors)) + } + }) + + t.Run("re-auth error", func(t *testing.T) { + initialCreds := NewBasicCredentials("user1", "pass1") + provider := newMockStreamingProvider(initialCreds) + + reauthErr := errors.New("re-auth failed") + var receivedErrors []error + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + return reauthErr + }, + func(err error) { + receivedErrors = append(receivedErrors, err) + }, + ) + + creds, cancel, err := provider.Subscribe(listener) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cancel == nil { + t.Fatal("expected cancel function to be non-nil") + } + if creds != initialCreds { + t.Fatalf("expected credentials %v, got %v", initialCreds, creds) + } + if len(receivedErrors) != 1 { + t.Fatalf("expected 1 error, got %d", len(receivedErrors)) + } + if receivedErrors[0] != reauthErr { + t.Fatalf("expected error %v, got %v", reauthErr, receivedErrors[0]) + } + + if err := cancel(); err != nil { + t.Fatalf("unexpected error cancelling subscription: %v", err) + } + }) +} + +func TestBasicCredentials(t *testing.T) { + t.Run("basic auth", func(t *testing.T) { + creds := NewBasicCredentials("user1", "pass1") + username, password := creds.BasicAuth() + if username != "user1" { + t.Fatalf("expected username 'user1', got '%s'", username) + } + if password != "pass1" { + t.Fatalf("expected password 'pass1', got '%s'", password) + } + }) + + t.Run("raw credentials", func(t *testing.T) { + creds := NewBasicCredentials("user1", "pass1") + raw := creds.RawCredentials() + expected := "user1:pass1" + if raw != expected { + t.Fatalf("expected raw credentials '%s', got '%s'", expected, raw) + } + }) + + t.Run("empty username", func(t *testing.T) { + creds := NewBasicCredentials("", "pass1") + username, password := creds.BasicAuth() + if username != "" { + t.Fatalf("expected empty username, got '%s'", username) + } + if password != "pass1" { + t.Fatalf("expected password 'pass1', got '%s'", password) + } + }) +} + +func TestReAuthCredentialsListener(t *testing.T) { + t.Run("successful re-auth", func(t *testing.T) { + var reAuthCalled bool + var onErrCalled bool + var receivedCreds Credentials + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + reAuthCalled = true + receivedCreds = creds + return nil + }, + func(err error) { + onErrCalled = true + }, + ) + + creds := NewBasicCredentials("user1", "pass1") + listener.OnNext(creds) + + if !reAuthCalled { + t.Fatal("expected reAuth to be called") + } + if onErrCalled { + t.Fatal("expected onErr not to be called") + } + if receivedCreds != creds { + t.Fatalf("expected credentials %v, got %v", creds, receivedCreds) + } + }) + + t.Run("re-auth error", func(t *testing.T) { + var reAuthCalled bool + var onErrCalled bool + var receivedErr error + expectedErr := errors.New("re-auth failed") + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + reAuthCalled = true + return expectedErr + }, + func(err error) { + onErrCalled = true + receivedErr = err + }, + ) + + creds := NewBasicCredentials("user1", "pass1") + listener.OnNext(creds) + + if !reAuthCalled { + t.Fatal("expected reAuth to be called") + } + if !onErrCalled { + t.Fatal("expected onErr to be called") + } + if receivedErr != expectedErr { + t.Fatalf("expected error %v, got %v", expectedErr, receivedErr) + } + }) + + t.Run("on error", func(t *testing.T) { + var onErrCalled bool + var receivedErr error + expectedErr := errors.New("provider error") + + listener := NewReAuthCredentialsListener( + func(creds Credentials) error { + return nil + }, + func(err error) { + onErrCalled = true + receivedErr = err + }, + ) + + listener.OnError(expectedErr) + + if !onErrCalled { + t.Fatal("expected onErr to be called") + } + if receivedErr != expectedErr { + t.Fatalf("expected error %v, got %v", expectedErr, receivedErr) + } + }) + + t.Run("nil callbacks", func(t *testing.T) { + listener := NewReAuthCredentialsListener(nil, nil) + + // Should not panic + listener.OnNext(NewBasicCredentials("user1", "pass1")) + listener.OnError(errors.New("test error")) + }) +} diff --git a/auth/reauth_credentials_listener.go b/auth/reauth_credentials_listener.go new file mode 100644 index 0000000000..40076a0b13 --- /dev/null +++ b/auth/reauth_credentials_listener.go @@ -0,0 +1,47 @@ +package auth + +// ReAuthCredentialsListener is a struct that implements the CredentialsListener interface. +// It is used to re-authenticate the credentials when they are updated. +// It contains: +// - reAuth: a function that takes the new credentials and returns an error if any. +// - onErr: a function that takes an error and handles it. +type ReAuthCredentialsListener struct { + reAuth func(credentials Credentials) error + onErr func(err error) +} + +// OnNext is called when the credentials are updated. +// It calls the reAuth function with the new credentials. +// If the reAuth function returns an error, it calls the onErr function with the error. +func (c *ReAuthCredentialsListener) OnNext(credentials Credentials) { + if c.reAuth == nil { + return + } + + err := c.reAuth(credentials) + if err != nil { + c.OnError(err) + } +} + +// OnError is called when an error occurs. +// It can be called from both the credentials provider and the reAuth function. +func (c *ReAuthCredentialsListener) OnError(err error) { + if c.onErr == nil { + return + } + + c.onErr(err) +} + +// NewReAuthCredentialsListener creates a new ReAuthCredentialsListener. +// Implements the auth.CredentialsListener interface. +func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener { + return &ReAuthCredentialsListener{ + reAuth: reAuth, + onErr: onErr, + } +} + +// Ensure ReAuthCredentialsListener implements the CredentialsListener interface. +var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) diff --git a/command_recorder_test.go b/command_recorder_test.go new file mode 100644 index 0000000000..2251df5ef6 --- /dev/null +++ b/command_recorder_test.go @@ -0,0 +1,86 @@ +package redis_test + +import ( + "context" + "strings" + "sync" + + "github.com/redis/go-redis/v9" +) + +// commandRecorder records the last N commands executed by a Redis client. +type commandRecorder struct { + mu sync.Mutex + commands []string + maxSize int +} + +// newCommandRecorder creates a new command recorder with the specified maximum size. +func newCommandRecorder(maxSize int) *commandRecorder { + return &commandRecorder{ + commands: make([]string, 0, maxSize), + maxSize: maxSize, + } +} + +// Record adds a command to the recorder. +func (r *commandRecorder) Record(cmd string) { + cmd = strings.ToLower(cmd) + r.mu.Lock() + defer r.mu.Unlock() + + r.commands = append(r.commands, cmd) + if len(r.commands) > r.maxSize { + r.commands = r.commands[1:] + } +} + +// LastCommands returns a copy of the recorded commands. +func (r *commandRecorder) LastCommands() []string { + r.mu.Lock() + defer r.mu.Unlock() + return append([]string(nil), r.commands...) +} + +// Contains checks if the recorder contains a specific command. +func (r *commandRecorder) Contains(cmd string) bool { + cmd = strings.ToLower(cmd) + r.mu.Lock() + defer r.mu.Unlock() + for _, c := range r.commands { + if strings.Contains(c, cmd) { + return true + } + } + return false +} + +// Hook returns a Redis hook that records commands. +func (r *commandRecorder) Hook() redis.Hook { + return &commandHook{recorder: r} +} + +// commandHook implements the redis.Hook interface to record commands. +type commandHook struct { + recorder *commandRecorder +} + +func (h *commandHook) DialHook(next redis.DialHook) redis.DialHook { + return next +} + +func (h *commandHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { + return func(ctx context.Context, cmd redis.Cmder) error { + h.recorder.Record(cmd.String()) + return next(ctx, cmd) + } +} + +func (h *commandHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { + return func(ctx context.Context, cmds []redis.Cmder) error { + for _, cmd := range cmds { + h.recorder.Record(cmd.String()) + } + return next(ctx, cmds) + } +} diff --git a/doctests/lpush_lrange_test.go b/doctests/lpush_lrange_test.go index 1e69f4b0a2..4c5a03a38d 100644 --- a/doctests/lpush_lrange_test.go +++ b/doctests/lpush_lrange_test.go @@ -5,6 +5,7 @@ package example_commands_test import ( "context" "fmt" + "time" "github.com/redis/go-redis/v9" ) @@ -33,6 +34,7 @@ func ExampleClient_LPush_and_lrange() { } fmt.Println(listSize) + time.Sleep(10 * time.Millisecond) // Simulate some delay value, err := rdb.LRange(ctx, "my_bikes", 0, -1).Result() if err != nil { diff --git a/example_instrumentation_test.go b/example_instrumentation_test.go index a6069cf3f1..36234ff09e 100644 --- a/example_instrumentation_test.go +++ b/example_instrumentation_test.go @@ -23,38 +23,47 @@ func (redisHook) DialHook(hook redis.DialHook) redis.DialHook { func (redisHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - fmt.Printf("starting processing: <%s>\n", cmd) + fmt.Printf("starting processing: <%v>\n", cmd.Args()) err := hook(ctx, cmd) - fmt.Printf("finished processing: <%s>\n", cmd) + fmt.Printf("finished processing: <%v>\n", cmd.Args()) return err } } func (redisHook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { - fmt.Printf("pipeline starting processing: %v\n", cmds) + names := make([]string, 0, len(cmds)) + for _, cmd := range cmds { + names = append(names, fmt.Sprintf("%v", cmd.Args())) + } + fmt.Printf("pipeline starting processing: %v\n", names) err := hook(ctx, cmds) - fmt.Printf("pipeline finished processing: %v\n", cmds) + fmt.Printf("pipeline finished processing: %v\n", names) return err } } func Example_instrumentation() { rdb := redis.NewClient(&redis.Options{ - Addr: ":6379", + Addr: ":6379", + DisableIdentity: true, }) rdb.AddHook(redisHook{}) rdb.Ping(ctx) - // Output: starting processing: + // Output: + // starting processing: <[ping]> // dialing tcp :6379 // finished dialing tcp :6379 - // finished processing: + // starting processing: <[hello 3]> + // finished processing: <[hello 3]> + // finished processing: <[ping]> } func ExamplePipeline_instrumentation() { rdb := redis.NewClient(&redis.Options{ - Addr: ":6379", + Addr: ":6379", + DisableIdentity: true, }) rdb.AddHook(redisHook{}) @@ -63,15 +72,19 @@ func ExamplePipeline_instrumentation() { pipe.Ping(ctx) return nil }) - // Output: pipeline starting processing: [ping: ping: ] + // Output: + // pipeline starting processing: [[ping] [ping]] // dialing tcp :6379 // finished dialing tcp :6379 - // pipeline finished processing: [ping: PONG ping: PONG] + // starting processing: <[hello 3]> + // finished processing: <[hello 3]> + // pipeline finished processing: [[ping] [ping]] } func ExampleClient_Watch_instrumentation() { rdb := redis.NewClient(&redis.Options{ - Addr: ":6379", + Addr: ":6379", + DisableIdentity: true, }) rdb.AddHook(redisHook{}) @@ -81,14 +94,16 @@ func ExampleClient_Watch_instrumentation() { return nil }, "foo") // Output: - // starting processing: + // starting processing: <[watch foo]> // dialing tcp :6379 // finished dialing tcp :6379 - // finished processing: - // starting processing: - // finished processing: - // starting processing: - // finished processing: - // starting processing: - // finished processing: + // starting processing: <[hello 3]> + // finished processing: <[hello 3]> + // finished processing: <[watch foo]> + // starting processing: <[ping]> + // finished processing: <[ping]> + // starting processing: <[ping]> + // finished processing: <[ping]> + // starting processing: <[unwatch]> + // finished processing: <[unwatch]> } diff --git a/internal/pool/conn.go b/internal/pool/conn.go index 7f45bc0bb7..c1087b401a 100644 --- a/internal/pool/conn.go +++ b/internal/pool/conn.go @@ -23,6 +23,8 @@ type Conn struct { Inited bool pooled bool createdAt time.Time + + onClose func() error } func NewConn(netConn net.Conn) *Conn { @@ -46,6 +48,10 @@ func (cn *Conn) SetUsedAt(tm time.Time) { atomic.StoreInt64(&cn.usedAt, tm.Unix()) } +func (cn *Conn) SetOnClose(fn func() error) { + cn.onClose = fn +} + func (cn *Conn) SetNetConn(netConn net.Conn) { cn.netConn = netConn cn.rd.Reset(netConn) @@ -95,6 +101,10 @@ func (cn *Conn) WithWriter( } func (cn *Conn) Close() error { + if cn.onClose != nil { + // ignore error + _ = cn.onClose() + } return cn.netConn.Close() } diff --git a/internal_test.go b/internal_test.go index bd7d237a86..8f1f1f3121 100644 --- a/internal_test.go +++ b/internal_test.go @@ -212,10 +212,10 @@ func TestRingShardsCleanup(t *testing.T) { }, NewClient: func(opt *Options) *Client { c := NewClient(opt) - c.baseClient.onClose = func() error { + c.baseClient.onClose = c.baseClient.wrappedOnClose(func() error { closeCounter.increment(opt.Addr) return nil - } + }) return c }, }) @@ -261,10 +261,10 @@ func TestRingShardsCleanup(t *testing.T) { } createCounter.increment(opt.Addr) c := NewClient(opt) - c.baseClient.onClose = func() error { + c.baseClient.onClose = c.baseClient.wrappedOnClose(func() error { closeCounter.increment(opt.Addr) return nil - } + }) return c }, }) diff --git a/options.go b/options.go index bf6c032ec8..b87a234a41 100644 --- a/options.go +++ b/options.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal/pool" ) @@ -29,10 +30,13 @@ type Limiter interface { // Options keeps the settings to set up redis connection. type Options struct { - // The network type, either tcp or unix. - // Default is tcp. + + // Network type, either tcp or unix. + // + // default: is tcp. Network string - // host:port address. + + // Addr is the address formated as host:port Addr string // ClientName will execute the `CLIENT SETNAME ClientName` command for each conn. @@ -46,17 +50,21 @@ type Options struct { OnConnect func(ctx context.Context, cn *Conn) error // Protocol 2 or 3. Use the version to negotiate RESP version with redis-server. - // Default is 3. + // + // default: 3. Protocol int - // Use the specified Username to authenticate the current connection + + // Username is used to authenticate the current connection // with one of the connections defined in the ACL list when connecting // to a Redis 6.0 instance, or greater, that is using the Redis ACL system. Username string - // Optional password. Must match the password specified in the - // requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower), + + // Password is an optional password. Must match the password specified in the + // `requirepass` server configuration option (if connecting to a Redis 5.0 instance, or lower), // or the User Password when connecting to a Redis 6.0 instance, or greater, // that is using the Redis ACL system. Password string + // CredentialsProvider allows the username and password to be updated // before reconnecting. It should return the current username and password. CredentialsProvider func() (username string, password string) @@ -67,85 +75,126 @@ type Options struct { // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) - // Database to be selected after connecting to the server. + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + + // DB is the database to be selected after connecting to the server. DB int - // Maximum number of retries before giving up. - // Default is 3 retries; -1 (not 0) disables retries. + // MaxRetries is the maximum number of retries before giving up. + // -1 (not 0) disables retries. + // + // default: 3 retries MaxRetries int - // Minimum backoff between each retry. - // Default is 8 milliseconds; -1 disables backoff. + + // MinRetryBackoff is the minimum backoff between each retry. + // -1 disables backoff. + // + // default: 8 milliseconds MinRetryBackoff time.Duration - // Maximum backoff between each retry. - // Default is 512 milliseconds; -1 disables backoff. + + // MaxRetryBackoff is the maximum backoff between each retry. + // -1 disables backoff. + // default: 512 milliseconds; MaxRetryBackoff time.Duration - // Dial timeout for establishing new connections. - // Default is 5 seconds. + // DialTimeout for establishing new connections. + // + // default: 5 seconds DialTimeout time.Duration - // Timeout for socket reads. If reached, commands will fail + + // ReadTimeout for socket reads. If reached, commands will fail // with a timeout instead of blocking. Supported values: - // - `0` - default timeout (3 seconds). - // - `-1` - no timeout (block indefinitely). - // - `-2` - disables SetReadDeadline calls completely. + // + // - `-1` - no timeout (block indefinitely). + // - `-2` - disables SetReadDeadline calls completely. + // + // default: 3 seconds ReadTimeout time.Duration - // Timeout for socket writes. If reached, commands will fail + + // WriteTimeout for socket writes. If reached, commands will fail // with a timeout instead of blocking. Supported values: - // - `0` - default timeout (3 seconds). - // - `-1` - no timeout (block indefinitely). - // - `-2` - disables SetWriteDeadline calls completely. + // + // - `-1` - no timeout (block indefinitely). + // - `-2` - disables SetWriteDeadline calls completely. + // + // default: 3 seconds WriteTimeout time.Duration + // ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines. // See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts ContextTimeoutEnabled bool - // Type of connection pool. - // true for FIFO pool, false for LIFO pool. + // PoolFIFO type of connection pool. + // + // - true for FIFO pool + // - false for LIFO pool. + // // Note that FIFO has slightly higher overhead compared to LIFO, // but it helps closing idle connections faster reducing the pool size. PoolFIFO bool - // Base number of socket connections. + + // PoolSize is the base number of socket connections. // Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS. // If there is not enough connections in the pool, new connections will be allocated in excess of PoolSize, // you can limit it through MaxActiveConns + // + // default: 10 * runtime.GOMAXPROCS(0) PoolSize int - // Amount of time client waits for connection if all connections + + // PoolTimeout is the amount of time client waits for connection if all connections // are busy before returning an error. - // Default is ReadTimeout + 1 second. + // + // default: ReadTimeout + 1 second PoolTimeout time.Duration - // Minimum number of idle connections which is useful when establishing - // new connection is slow. - // Default is 0. the idle connections are not closed by default. + + // MinIdleConns is the minimum number of idle connections which is useful when establishing + // new connection is slow. The idle connections are not closed by default. + // + // default: 0 MinIdleConns int - // Maximum number of idle connections. - // Default is 0. the idle connections are not closed by default. + + // MaxIdleConns is the maximum number of idle connections. + // The idle connections are not closed by default. + // + // default: 0 MaxIdleConns int - // Maximum number of connections allocated by the pool at a given time. + + // MaxActiveConns is the maximum number of connections allocated by the pool at a given time. // When zero, there is no limit on the number of connections in the pool. + // If the pool is full, the next call to Get() will block until a connection is released. MaxActiveConns int + // ConnMaxIdleTime is the maximum amount of time a connection may be idle. // Should be less than server's timeout. // // Expired connections may be closed lazily before reuse. // If d <= 0, connections are not closed due to a connection's idle time. + // -1 disables idle timeout check. // - // Default is 30 minutes. -1 disables idle timeout check. + // default: 30 minutes ConnMaxIdleTime time.Duration + // ConnMaxLifetime is the maximum amount of time a connection may be reused. // // Expired connections may be closed lazily before reuse. // If <= 0, connections are not closed due to a connection's age. // - // Default is to not close idle connections. + // default: 0 ConnMaxLifetime time.Duration - // TLS Config to use. When set, TLS will be negotiated. + // TLSConfig to use. When set, TLS will be negotiated. TLSConfig *tls.Config // Limiter interface used to implement circuit breaker or rate limiter. Limiter Limiter - // Enables read only queries on slave/follower nodes. + // readOnly enables read only queries on slave/follower nodes. readOnly bool // DisableIndentity - Disable set-lib on connect. @@ -161,9 +210,11 @@ type Options struct { DisableIdentity bool // Add suffix to client name. Default is empty. + // IdentitySuffix - add suffix to client name. IdentitySuffix string // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. + // When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult UnstableResp3 bool } diff --git a/osscluster.go b/osscluster.go index cc2444fa9f..6c6b756380 100644 --- a/osscluster.go +++ b/osscluster.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "time" + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" "github.com/redis/go-redis/v9/internal/pool" @@ -66,11 +67,12 @@ type ClusterOptions struct { OnConnect func(ctx context.Context, cn *Conn) error - Protocol int - Username string - Password string - CredentialsProvider func() (username string, password string) - CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + Protocol int + Username string + Password string + CredentialsProvider func() (username string, password string) + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + StreamingCredentialsProvider auth.StreamingCredentialsProvider MaxRetries int MinRetryBackoff time.Duration @@ -292,11 +294,12 @@ func (opt *ClusterOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, - CredentialsProvider: opt.CredentialsProvider, - CredentialsProviderContext: opt.CredentialsProviderContext, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, MaxRetries: opt.MaxRetries, MinRetryBackoff: opt.MinRetryBackoff, diff --git a/osscluster_test.go b/osscluster_test.go index ccf6daad8f..6e214a7191 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -89,6 +89,9 @@ func (s *clusterScenario) newClusterClient( func (s *clusterScenario) Close() error { ctx := context.TODO() for _, master := range s.masters() { + if master == nil { + continue + } err := master.FlushAll(ctx).Err() if err != nil { return err diff --git a/probabilistic_test.go b/probabilistic_test.go index a0a050e23e..0a3f1a15c8 100644 --- a/probabilistic_test.go +++ b/probabilistic_test.go @@ -298,7 +298,7 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { }) It("should CFCount", Label("cuckoo", "cfcount"), func() { - err := client.CFAdd(ctx, "testcf1", "item1").Err() + client.CFAdd(ctx, "testcf1", "item1") cnt, err := client.CFCount(ctx, "testcf1", "item1").Result() Expect(err).NotTo(HaveOccurred()) Expect(cnt).To(BeEquivalentTo(int64(1))) @@ -394,7 +394,7 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { NoCreate: true, } - result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + _, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() Expect(err).To(HaveOccurred()) args = &redis.CFInsertOptions{ @@ -402,7 +402,7 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { NoCreate: false, } - result, err = client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() Expect(err).NotTo(HaveOccurred()) Expect(len(result)).To(BeEquivalentTo(3)) }) diff --git a/redis.go b/redis.go index 19ff57b017..bafe82f752 100644 --- a/redis.go +++ b/redis.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hscan" "github.com/redis/go-redis/v9/internal/pool" @@ -203,6 +204,7 @@ func (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) e type baseClient struct { opt *Options connPool pool.Pooler + hooksMixin onClose func() error // hook called when client is closed } @@ -282,30 +284,107 @@ func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) { return cn, nil } +func (c *baseClient) newReAuthCredentialsListener(poolCn *pool.Conn) auth.CredentialsListener { + return auth.NewReAuthCredentialsListener( + c.reAuthConnection(poolCn), + c.onAuthenticationErr(poolCn), + ) +} + +func (c *baseClient) reAuthConnection(poolCn *pool.Conn) func(credentials auth.Credentials) error { + return func(credentials auth.Credentials) error { + var err error + username, password := credentials.BasicAuth() + ctx := context.Background() + connPool := pool.NewSingleConnPool(c.connPool, poolCn) + // hooksMixin are intentionally empty here + cn := newConn(c.opt, connPool, nil) + + if username != "" { + err = cn.AuthACL(ctx, username, password).Err() + } else { + err = cn.Auth(ctx, password).Err() + } + return err + } +} +func (c *baseClient) onAuthenticationErr(poolCn *pool.Conn) func(err error) { + return func(err error) { + if err != nil { + if isBadConn(err, false, c.opt.Addr) { + // Close the connection to force a reconnection. + err := c.connPool.CloseConn(poolCn) + if err != nil { + internal.Logger.Printf(context.Background(), "redis: failed to close connection: %v", err) + // try to close the network connection directly + // so that no resource is leaked + err := poolCn.Close() + if err != nil { + internal.Logger.Printf(context.Background(), "redis: failed to close network connection: %v", err) + } + } + } + internal.Logger.Printf(context.Background(), "redis: re-authentication failed: %v", err) + } + } +} + +func (c *baseClient) wrappedOnClose(newOnClose func() error) func() error { + onClose := c.onClose + return func() error { + var firstErr error + err := newOnClose() + // Even if we have an error we would like to execute the onClose hook + // if it exists. We will return the first error that occurred. + // This is to keep error handling consistent with the rest of the code. + if err != nil { + firstErr = err + } + if onClose != nil { + err = onClose() + if err != nil && firstErr == nil { + firstErr = err + } + } + return firstErr + } +} + func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { if cn.Inited { return nil } - cn.Inited = true var err error - username, password := c.opt.Username, c.opt.Password - if c.opt.CredentialsProviderContext != nil { - if username, password, err = c.opt.CredentialsProviderContext(ctx); err != nil { - return err + cn.Inited = true + connPool := pool.NewSingleConnPool(c.connPool, cn) + conn := newConn(c.opt, connPool, &c.hooksMixin) + + username, password := "", "" + if c.opt.StreamingCredentialsProvider != nil { + credentials, unsubscribeFromCredentialsProvider, err := c.opt.StreamingCredentialsProvider. + Subscribe(c.newReAuthCredentialsListener(cn)) + if err != nil { + return fmt.Errorf("failed to subscribe to streaming credentials: %w", err) + } + c.onClose = c.wrappedOnClose(unsubscribeFromCredentialsProvider) + cn.SetOnClose(unsubscribeFromCredentialsProvider) + username, password = credentials.BasicAuth() + } else if c.opt.CredentialsProviderContext != nil { + username, password, err = c.opt.CredentialsProviderContext(ctx) + if err != nil { + return fmt.Errorf("failed to get credentials from context provider: %w", err) } } else if c.opt.CredentialsProvider != nil { username, password = c.opt.CredentialsProvider() + } else if c.opt.Username != "" || c.opt.Password != "" { + username, password = c.opt.Username, c.opt.Password } - connPool := pool.NewSingleConnPool(c.connPool, cn) - conn := newConn(c.opt, connPool) - - var auth bool // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { - auth = true + if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { + // Authentication successful with HELLO command } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal // execution result of the HELLO command, we consider it to be an indication that @@ -315,17 +394,19 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // with different error string results for unsupported commands, making it // difficult to rely on error strings to determine all results. return err + } else if password != "" { + // Try legacy AUTH command if HELLO failed + if username != "" { + err = conn.AuthACL(ctx, username, password).Err() + } else { + err = conn.Auth(ctx, password).Err() + } + if err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } } _, err = conn.Pipelined(ctx, func(pipe Pipeliner) error { - if !auth && password != "" { - if username != "" { - pipe.AuthACL(ctx, username, password) - } else { - pipe.Auth(ctx, password) - } - } - if c.opt.DB > 0 { pipe.Select(ctx, c.opt.DB) } @@ -341,7 +422,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return nil }) if err != nil { - return err + return fmt.Errorf("failed to initialize connection options: %w", err) } if !c.opt.DisableIdentity && !c.opt.DisableIndentity { @@ -363,6 +444,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { if c.opt.OnConnect != nil { return c.opt.OnConnect(ctx, conn) } + return nil } @@ -481,6 +563,16 @@ func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration { return c.opt.ReadTimeout } +// context returns the context for the current connection. +// If the context timeout is enabled, it returns the original context. +// Otherwise, it returns a new background context. +func (c *baseClient) context(ctx context.Context) context.Context { + if c.opt.ContextTimeoutEnabled { + return ctx + } + return context.Background() +} + // Close closes the client, releasing any open resources. // // It is rare to Close a Client, as the Client is meant to be @@ -633,13 +725,6 @@ func txPipelineReadQueued(rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) return nil } -func (c *baseClient) context(ctx context.Context) context.Context { - if c.opt.ContextTimeoutEnabled { - return ctx - } - return context.Background() -} - //------------------------------------------------------------------------------ // Client is a Redis client representing a pool of zero or more underlying connections. @@ -650,7 +735,6 @@ func (c *baseClient) context(ctx context.Context) context.Context { type Client struct { *baseClient cmdable - hooksMixin } // NewClient returns a client to the Redis Server specified by Options. @@ -689,7 +773,7 @@ func (c *Client) WithTimeout(timeout time.Duration) *Client { } func (c *Client) Conn() *Conn { - return newConn(c.opt, pool.NewStickyConnPool(c.connPool)) + return newConn(c.opt, pool.NewStickyConnPool(c.connPool), &c.hooksMixin) } // Do create a Cmd from the args and processes the cmd. @@ -822,10 +906,12 @@ type Conn struct { baseClient cmdable statefulCmdable - hooksMixin } -func newConn(opt *Options, connPool pool.Pooler) *Conn { +// newConn is a helper func to create a new Conn instance. +// the Conn instance is not thread-safe and should not be shared between goroutines. +// the parentHooks will be cloned, no need to clone before passing it. +func newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn { c := Conn{ baseClient: baseClient{ opt: opt, @@ -833,6 +919,10 @@ func newConn(opt *Options, connPool pool.Pooler) *Conn { }, } + if parentHooks != nil { + c.hooksMixin = parentHooks.clone() + } + c.cmdable = c.Process c.statefulCmdable = c.Process c.initHooks(hooks{ diff --git a/redis_test.go b/redis_test.go index 80e28341c1..dd14214f1a 100644 --- a/redis_test.go +++ b/redis_test.go @@ -14,6 +14,7 @@ import ( . "github.com/bsm/gomega" "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/auth" ) type redisHookError struct{} @@ -727,6 +728,174 @@ var _ = Describe("Dialer connection timeouts", func() { Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay)) }) }) + +var _ = Describe("Credentials Provider Priority", func() { + var client *redis.Client + var opt *redis.Options + var recorder *commandRecorder + + BeforeEach(func() { + recorder = newCommandRecorder(10) + }) + + AfterEach(func() { + if client != nil { + Expect(client.Close()).NotTo(HaveOccurred()) + } + }) + + It("should use streaming provider when available", func() { + streamingCreds := auth.NewBasicCredentials("streaming_user", "streaming_pass") + ctxCreds := auth.NewBasicCredentials("ctx_user", "ctx_pass") + providerCreds := auth.NewBasicCredentials("provider_user", "provider_pass") + + opt = &redis.Options{ + Username: "field_user", + Password: "field_pass", + CredentialsProvider: func() (string, string) { + username, password := providerCreds.BasicAuth() + return username, password + }, + CredentialsProviderContext: func(ctx context.Context) (string, string, error) { + username, password := ctxCreds.BasicAuth() + return username, password, nil + }, + StreamingCredentialsProvider: &mockStreamingProvider{ + credentials: streamingCreds, + updates: make(chan auth.Credentials, 1), + }, + } + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH streaming_user")).To(BeTrue()) + }) + + It("should use context provider when streaming provider is not available", func() { + ctxCreds := auth.NewBasicCredentials("ctx_user", "ctx_pass") + providerCreds := auth.NewBasicCredentials("provider_user", "provider_pass") + + opt = &redis.Options{ + Username: "field_user", + Password: "field_pass", + CredentialsProvider: func() (string, string) { + username, password := providerCreds.BasicAuth() + return username, password + }, + CredentialsProviderContext: func(ctx context.Context) (string, string, error) { + username, password := ctxCreds.BasicAuth() + return username, password, nil + }, + } + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH ctx_user")).To(BeTrue()) + }) + + It("should use regular provider when streaming and context providers are not available", func() { + providerCreds := auth.NewBasicCredentials("provider_user", "provider_pass") + + opt = &redis.Options{ + Username: "field_user", + Password: "field_pass", + CredentialsProvider: func() (string, string) { + username, password := providerCreds.BasicAuth() + return username, password + }, + } + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH provider_user")).To(BeTrue()) + }) + + It("should use username/password fields when no providers are set", func() { + opt = &redis.Options{ + Username: "field_user", + Password: "field_pass", + } + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH field_user")).To(BeTrue()) + }) + + It("should use empty credentials when nothing is set", func() { + opt = &redis.Options{} + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // no pass, ok + Expect(client.Ping(context.Background()).Err()).NotTo(HaveOccurred()) + Expect(recorder.Contains("AUTH")).To(BeFalse()) + }) + + It("should handle credential updates from streaming provider", func() { + initialCreds := auth.NewBasicCredentials("initial_user", "initial_pass") + updatedCreds := auth.NewBasicCredentials("updated_user", "updated_pass") + updatesChan := make(chan auth.Credentials, 1) + + opt = &redis.Options{ + StreamingCredentialsProvider: &mockStreamingProvider{ + credentials: initialCreds, + updates: updatesChan, + }, + } + + client = redis.NewClient(opt) + client.AddHook(recorder.Hook()) + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH initial_user")).To(BeTrue()) + + // Update credentials + opt.StreamingCredentialsProvider.(*mockStreamingProvider).updates <- updatedCreds + // wrongpass + Expect(client.Ping(context.Background()).Err()).To(HaveOccurred()) + Expect(recorder.Contains("AUTH updated_user")).To(BeTrue()) + close(updatesChan) + }) +}) + +type mockStreamingProvider struct { + credentials auth.Credentials + err error + updates chan auth.Credentials +} + +func (m *mockStreamingProvider) Subscribe(listener auth.CredentialsListener) (auth.Credentials, auth.UnsubscribeFunc, error) { + if m.err != nil { + return nil, nil, m.err + } + + // Start goroutine to handle updates + go func() { + for creds := range m.updates { + m.credentials = creds + listener.OnNext(creds) + } + }() + + return m.credentials, func() (err error) { + defer func() { + if r := recover(); r != nil { + // this is just a mock: + // allow multiple closes from multiple listeners + } + }() + return + }, nil +} + var _ = Describe("Client creation", func() { Context("simple client with nil options", func() { It("panics", func() { diff --git a/ring_test.go b/ring_test.go index cfd545c178..599f6888aa 100644 --- a/ring_test.go +++ b/ring_test.go @@ -357,13 +357,17 @@ var _ = Describe("Redis Ring", func() { ring.AddHook(&hook{ processPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { - Expect(cmds).To(HaveLen(1)) + // skip the connection initialization + if cmds[0].Name() == "hello" || cmds[0].Name() == "client" { + return nil + } + Expect(len(cmds)).To(BeNumerically(">", 0)) Expect(cmds[0].String()).To(Equal("ping: ")) stack = append(stack, "ring.BeforeProcessPipeline") err := hook(ctx, cmds) - Expect(cmds).To(HaveLen(1)) + Expect(len(cmds)).To(BeNumerically(">", 0)) Expect(cmds[0].String()).To(Equal("ping: PONG")) stack = append(stack, "ring.AfterProcessPipeline") @@ -376,13 +380,17 @@ var _ = Describe("Redis Ring", func() { shard.AddHook(&hook{ processPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { - Expect(cmds).To(HaveLen(1)) + // skip the connection initialization + if cmds[0].Name() == "hello" || cmds[0].Name() == "client" { + return nil + } + Expect(len(cmds)).To(BeNumerically(">", 0)) Expect(cmds[0].String()).To(Equal("ping: ")) stack = append(stack, "shard.BeforeProcessPipeline") err := hook(ctx, cmds) - Expect(cmds).To(HaveLen(1)) + Expect(len(cmds)).To(BeNumerically(">", 0)) Expect(cmds[0].String()).To(Equal("ping: PONG")) stack = append(stack, "shard.AfterProcessPipeline") @@ -416,14 +424,18 @@ var _ = Describe("Redis Ring", func() { processPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { defer GinkgoRecover() + // skip the connection initialization + if cmds[0].Name() == "hello" || cmds[0].Name() == "client" { + return nil + } - Expect(cmds).To(HaveLen(3)) + Expect(len(cmds)).To(BeNumerically(">=", 3)) Expect(cmds[1].String()).To(Equal("ping: ")) stack = append(stack, "ring.BeforeProcessPipeline") err := hook(ctx, cmds) - Expect(cmds).To(HaveLen(3)) + Expect(len(cmds)).To(BeNumerically(">=", 3)) Expect(cmds[1].String()).To(Equal("ping: PONG")) stack = append(stack, "ring.AfterProcessPipeline") @@ -437,14 +449,18 @@ var _ = Describe("Redis Ring", func() { processPipelineHook: func(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { defer GinkgoRecover() + // skip the connection initialization + if cmds[0].Name() == "hello" || cmds[0].Name() == "client" { + return nil + } - Expect(cmds).To(HaveLen(3)) + Expect(len(cmds)).To(BeNumerically(">=", 3)) Expect(cmds[1].String()).To(Equal("ping: ")) stack = append(stack, "shard.BeforeProcessPipeline") err := hook(ctx, cmds) - Expect(cmds).To(HaveLen(3)) + Expect(len(cmds)).To(BeNumerically(">=", 3)) Expect(cmds[1].String()).To(Equal("ping: PONG")) stack = append(stack, "shard.AfterProcessPipeline") diff --git a/sentinel.go b/sentinel.go index 314bde1ef6..43fbcd2443 100644 --- a/sentinel.go +++ b/sentinel.go @@ -404,7 +404,7 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client { connPool = newConnPool(opt, rdb.dialHook) rdb.connPool = connPool - rdb.onClose = failover.Close + rdb.onClose = rdb.wrappedOnClose(failover.Close) failover.mu.Lock() failover.onFailover = func(ctx context.Context, addr string) { @@ -455,7 +455,6 @@ func masterReplicaDialer( // SentinelClient is a client for a Redis Sentinel. type SentinelClient struct { *baseClient - hooksMixin } func NewSentinelClient(opt *Options) *SentinelClient { diff --git a/tx.go b/tx.go index 039eaf3516..0daa222e35 100644 --- a/tx.go +++ b/tx.go @@ -19,16 +19,15 @@ type Tx struct { baseClient cmdable statefulCmdable - hooksMixin } func (c *Client) newTx() *Tx { tx := Tx{ baseClient: baseClient{ - opt: c.opt, - connPool: pool.NewStickyConnPool(c.connPool), + opt: c.opt, + connPool: pool.NewStickyConnPool(c.connPool), + hooksMixin: c.hooksMixin.clone(), }, - hooksMixin: c.hooksMixin.clone(), } tx.init() return &tx From b50d0ad810ef1a32457f745317ef979460337314 Mon Sep 17 00:00:00 2001 From: Yi Deng <151997860+DengY11@users.noreply.github.com> Date: Tue, 27 May 2025 23:04:04 +0800 Subject: [PATCH 183/230] feat(ring): add GetShardClients and GetShardClientForKey methods to Ring for shard access (#3388) * feat: expose shard information in redis.Ring - Add GetShards() method to retrieve a list of active shard clients. - Add GetShardByKey(key string) method to get the shard client for a specific key. - These methods enable users to manage Pub/Sub operations more effectively by accessing shard-specific clients. * rename GetShardClients and GetShardClientForKey --------- Co-authored-by: DengY11 <212294929@qq.com> Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- ring.go | 23 +++++++++++++++ ring_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/ring.go b/ring.go index fe8a6dc47b..8a004b8c0e 100644 --- a/ring.go +++ b/ring.go @@ -847,3 +847,26 @@ func (c *Ring) Close() error { return c.sharding.Close() } + +// GetShardClients returns a list of all shard clients in the ring. +// This can be used to create dedicated connections (e.g., PubSub) for each shard. +func (c *Ring) GetShardClients() []*Client { + shards := c.sharding.List() + clients := make([]*Client, 0, len(shards)) + for _, shard := range shards { + if shard.IsUp() { + clients = append(clients, shard.Client) + } + } + return clients +} + +// GetShardClientForKey returns the shard client that would handle the given key. +// This can be used to determine which shard a particular key/channel would be routed to. +func (c *Ring) GetShardClientForKey(key string) (*Client, error) { + shard, err := c.sharding.GetByKey(key) + if err != nil { + return nil, err + } + return shard.Client, nil +} diff --git a/ring_test.go b/ring_test.go index 599f6888aa..aaac74dc9d 100644 --- a/ring_test.go +++ b/ring_test.go @@ -782,3 +782,82 @@ var _ = Describe("Ring Tx timeout", func() { testTimeout() }) }) + +var _ = Describe("Ring GetShardClients and GetShardClientForKey", func() { + var ring *redis.Ring + + BeforeEach(func() { + ring = redis.NewRing(&redis.RingOptions{ + Addrs: map[string]string{ + "shard1": ":6379", + "shard2": ":6380", + }, + }) + }) + + AfterEach(func() { + Expect(ring.Close()).NotTo(HaveOccurred()) + }) + + It("GetShardClients returns active shard clients", func() { + shards := ring.GetShardClients() + // Note: This test will pass even if Redis servers are not running, + // because GetShardClients only returns clients that are marked as "up", + // and newly created shards start as "up" until the first health check fails. + + if len(shards) == 0 { + // Expected if Redis servers are not running + Skip("No active shards found (Redis servers not running)") + } else { + Expect(len(shards)).To(BeNumerically(">", 0)) + for _, client := range shards { + Expect(client).NotTo(BeNil()) + } + } + }) + + It("GetShardClientForKey returns correct shard for keys", func() { + testKeys := []string{"key1", "key2", "user:123", "channel:test"} + + for _, key := range testKeys { + client, err := ring.GetShardClientForKey(key) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + } + }) + + It("GetShardClientForKey is consistent for same key", func() { + key := "test:consistency" + + // Call GetShardClientForKey multiple times with the same key + // Should always return the same shard + var firstClient *redis.Client + for i := 0; i < 5; i++ { + client, err := ring.GetShardClientForKey(key) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + + if i == 0 { + firstClient = client + } else { + Expect(client.String()).To(Equal(firstClient.String())) + } + } + }) + + It("GetShardClientForKey distributes keys across shards", func() { + testKeys := []string{"key1", "key2", "key3", "key4", "key5"} + shardMap := make(map[string]int) + + for _, key := range testKeys { + client, err := ring.GetShardClientForKey(key) + Expect(err).NotTo(HaveOccurred()) + shardMap[client.String()]++ + } + + // Should have at least 1 shard (could be all keys go to same shard due to hashing) + Expect(len(shardMap)).To(BeNumerically(">=", 1)) + // But with multiple keys, we expect some distribution + Expect(len(shardMap)).To(BeNumerically("<=", 2)) // At most 2 shards (our setup) + }) +}) From ebc18f09725d8170041adad6646753882583c8cc Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 27 May 2025 19:00:07 +0300 Subject: [PATCH 184/230] release(go-redis): v9.9.0 (#3390) * release(go-redis): v9.9.0 - Add release notes - Update dependencies and version.go * chore(release-notes) Update release notes --- RELEASE-NOTES.md | 57 +++++++++++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +-- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 +- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 +- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 13 files changed, 73 insertions(+), 16 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index fa106cb924..fc9ed2aa44 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,62 @@ # Release Notes +# 9.9.0 (2024-03-21) + +## 🚀 Highlights +- **Token-based Authentication**: Added `StreamingCredentialsProvider` for dynamic credential updates (experimental) + - Can be used with [go-redis-entraid](https://github.com/redis/go-redis-entraid) for Azure AD authentication +- **Connection Statistics**: Added connection waiting statistics for better monitoring +- **Failover Improvements**: Added `ParseFailoverURL` for easier failover configuration +- **Ring Client Enhancements**: Added shard access methods for better Pub/Sub management + +## ✨ New Features +- Added `StreamingCredentialsProvider` for token-based authentication ([#3320](https://github.com/redis/go-redis/pull/3320)) + - Supports dynamic credential updates + - Includes connection close hooks + - Note: Currently marked as experimental +- Added `ParseFailoverURL` for parsing failover URLs ([#3362](https://github.com/redis/go-redis/pull/3362)) +- Added connection waiting statistics ([#2804](https://github.com/redis/go-redis/pull/2804)) +- Added new utility functions: + - `ParseFloat` and `MustParseFloat` in public utils package ([#3371](https://github.com/redis/go-redis/pull/3371)) + - Unit tests for `Atoi`, `ParseInt`, `ParseUint`, and `ParseFloat` ([#3377](https://github.com/redis/go-redis/pull/3377)) +- Added Ring client shard access methods: + - `GetShardClients()` to retrieve all active shard clients + - `GetShardClientForKey(key string)` to get the shard client for a specific key ([#3388](https://github.com/redis/go-redis/pull/3388)) + +## 🐛 Bug Fixes +- Fixed routing reads to loading slave nodes ([#3370](https://github.com/redis/go-redis/pull/3370)) +- Added support for nil lag in XINFO GROUPS ([#3369](https://github.com/redis/go-redis/pull/3369)) +- Fixed pool acquisition timeout issues ([#3381](https://github.com/redis/go-redis/pull/3381)) +- Optimized unnecessary copy operations ([#3376](https://github.com/redis/go-redis/pull/3376)) + +## 📚 Documentation +- Updated documentation for XINFO GROUPS with nil lag support ([#3369](https://github.com/redis/go-redis/pull/3369)) +- Added package-level comments for new features + +## ⚡ Performance and Reliability +- Optimized `ReplaceSpaces` function ([#3383](https://github.com/redis/go-redis/pull/3383)) +- Set default value for `Options.Protocol` in `init()` ([#3387](https://github.com/redis/go-redis/pull/3387)) +- Exported pool errors for public consumption ([#3380](https://github.com/redis/go-redis/pull/3380)) + +## 🔧 Dependencies and Infrastructure +- Updated Redis CI to version 8.0.1 ([#3372](https://github.com/redis/go-redis/pull/3372)) +- Updated spellcheck GitHub Actions ([#3389](https://github.com/redis/go-redis/pull/3389)) +- Removed unused parameters ([#3382](https://github.com/redis/go-redis/pull/3382), [#3384](https://github.com/redis/go-redis/pull/3384)) + +## 🧪 Testing +- Added unit tests for pool acquisition timeout ([#3381](https://github.com/redis/go-redis/pull/3381)) +- Added unit tests for utility functions ([#3377](https://github.com/redis/go-redis/pull/3377)) + +## 👥 Contributors + +We would like to thank all the contributors who made this release possible: + +[@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@LINKIWI](https://github.com/LINKIWI), [@iamamirsalehi](https://github.com/iamamirsalehi), [@fukua95](https://github.com/fukua95), [@lzakharov](https://github.com/lzakharov), [@DengY11](https://github.com/DengY11) + +## 📝 Changelog + +For a complete list of changes, see the [full changelog](https://github.com/redis/go-redis/compare/v9.8.0...v9.9.0). + # 9.8.0 (2025-04-30) ## 🚀 Highlights diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 6d731f370d..6a193952ab 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.9.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 28edd2caf2..89af916aca 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0 +require github.com/redis/go-redis/v9 v9.9.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index c10579f13e..d56b1608ea 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.9.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 50964fa8ea..bec4100d87 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0 +require github.com/redis/go-redis/v9 v9.9.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index da45631037..989f21af1b 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.8.0 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/extra/redisotel/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.9.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 86f25db7b6..392bab9763 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.8.0 +require github.com/redis/go-redis/v9 v9.9.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index c10579f13e..d56b1608ea 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.9.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 65499e2c7c..66ce5aedc9 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.9.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 20c78b9d15..ff3446beed 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.9.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index c9c63427b5..639c333cd7 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.8.0 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.9.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 25193e4655..fc022923fa 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.8.0 + github.com/redis/go-redis/v9 v9.9.0 ) require ( diff --git a/version.go b/version.go index c56e04ff14..24a037f352 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.8.0" + return "9.9.0" } From 46121953028caf3b8f007e916dd7e7631397ef0b Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 28 May 2025 10:30:31 +0300 Subject: [PATCH 185/230] Update RELEASE-NOTES.md (#3391) --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index fc9ed2aa44..7b6ce5cd8a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,6 +1,6 @@ # Release Notes -# 9.9.0 (2024-03-21) +# 9.9.0 (2025-05-27) ## 🚀 Highlights - **Token-based Authentication**: Added `StreamingCredentialsProvider` for dynamic credential updates (experimental) From 8b8ff5a9f118574b402d3095584a408b5642fc72 Mon Sep 17 00:00:00 2001 From: Nicholas Page Date: Tue, 3 Jun 2025 03:27:54 -0700 Subject: [PATCH 186/230] chore(tests): add the missing NewFloatSliceResult for testing (#3393) --- result.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/result.go b/result.go index cfd4cf92ed..3e0d0a1348 100644 --- a/result.go +++ b/result.go @@ -82,6 +82,14 @@ func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd { return &cmd } +// NewFloatSliceResult returns a FloatSliceCmd initialised with val and err for testing. +func NewFloatSliceResult(val []float64, err error) *FloatSliceCmd { + var cmd FloatSliceCmd + cmd.val = val + cmd.SetErr(err) + return &cmd +} + // NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing. func NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd { var cmd MapStringStringCmd From bd9ec100effe02fd9abac499e0bce9c31ea1f2ab Mon Sep 17 00:00:00 2001 From: fukua95 Date: Thu, 5 Jun 2025 16:35:45 +0800 Subject: [PATCH 187/230] feat: support vectorset (#3375) * feat: support vectorset * fix: char encoding error * use `any` instread of `interface{}` * update vectorset API Signed-off-by: fukua95 * refact: MapStringFloat64Cmd -> VectorInfoSliceCmd Signed-off-by: fukua95 * update: * the type of vector attribute: string -> VectorAttributeMarshaller * Add a new API VRemAttr * mark the APIs are experimental Signed-off-by: fukua95 * trigger CI again Signed-off-by: fukua95 * rename a API: VRemAttr -> VClearAttributes Signed-off-by: fukua95 * add test Signed-off-by: fukua95 * feat(vectorset): improve VSetAttr API and add comprehensive test suite - Simplify VSetAttr to accept interface{} with automatic JSON marshalling - Remove VectorAttributeMarshaller interface for simpler API - Add comprehensive unit tests for all vectorset commands --------- Signed-off-by: fukua95 Co-authored-by: Nedyalko Dyakov --- command.go | 56 +++ commands.go | 1 + unit_test.go | 26 ++ vectorset_commands.go | 348 ++++++++++++++++ vectorset_commands_integration_test.go | 326 +++++++++++++++ vectorset_commands_test.go | 542 +++++++++++++++++++++++++ 6 files changed, 1299 insertions(+) create mode 100644 unit_test.go create mode 100644 vectorset_commands.go create mode 100644 vectorset_commands_integration_test.go create mode 100644 vectorset_commands_test.go diff --git a/command.go b/command.go index 5fa347f43f..56b2257214 100644 --- a/command.go +++ b/command.go @@ -5620,3 +5620,59 @@ func (cmd *MonitorCmd) Stop() { defer cmd.mu.Unlock() cmd.status = monitorStatusStop } + +type VectorScoreSliceCmd struct { + baseCmd + + val []VectorScore +} + +var _ Cmder = (*VectorScoreSliceCmd)(nil) + +func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { + return &VectorScoreSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) { + cmd.val = val +} + +func (cmd *VectorScoreSliceCmd) Val() []VectorScore { + return cmd.val +} + +func (cmd *VectorScoreSliceCmd) Result() ([]VectorScore, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorScoreSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + + cmd.val = make([]VectorScore, n) + for i := 0; i < n; i++ { + name, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[i].Name = name + + score, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[i].Score = score + } + return nil +} diff --git a/commands.go b/commands.go index 2713232420..c0358001d1 100644 --- a/commands.go +++ b/commands.go @@ -234,6 +234,7 @@ type Cmdable interface { StreamCmdable TimeseriesCmdable JSONCmdable + VectorSetCmdable } type StatefulCmdable interface { diff --git a/unit_test.go b/unit_test.go new file mode 100644 index 0000000000..e4d0e7b57d --- /dev/null +++ b/unit_test.go @@ -0,0 +1,26 @@ +package redis + +import ( + "context" +) + +// mockCmdable is a mock implementation of cmdable that records the last command. +// This is used for unit testing command construction without requiring a Redis server. +type mockCmdable struct { + lastCmd Cmder + returnErr error +} + +func (m *mockCmdable) call(ctx context.Context, cmd Cmder) error { + m.lastCmd = cmd + if m.returnErr != nil { + cmd.SetErr(m.returnErr) + } + return m.returnErr +} + +func (m *mockCmdable) asCmdable() cmdable { + return func(ctx context.Context, cmd Cmder) error { + return m.call(ctx, cmd) + } +} diff --git a/vectorset_commands.go b/vectorset_commands.go new file mode 100644 index 0000000000..2bd9e22166 --- /dev/null +++ b/vectorset_commands.go @@ -0,0 +1,348 @@ +package redis + +import ( + "context" + "encoding/json" + "strconv" +) + +// note: the APIs is experimental and may be subject to change. +type VectorSetCmdable interface { + VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd + VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd + VCard(ctx context.Context, key string) *IntCmd + VDim(ctx context.Context, key string) *IntCmd + VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd + VGetAttr(ctx context.Context, key, element string) *StringCmd + VInfo(ctx context.Context, key string) *MapStringInterfaceCmd + VLinks(ctx context.Context, key, element string) *StringSliceCmd + VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd + VRandMember(ctx context.Context, key string) *StringCmd + VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd + VRem(ctx context.Context, key, element string) *BoolCmd + VSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd + VClearAttributes(ctx context.Context, key, element string) *BoolCmd + VSim(ctx context.Context, key string, val Vector) *StringSliceCmd + VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd + VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd + VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd +} + +type Vector interface { + Value() []any +} + +const ( + vectorFormatFP32 string = "FP32" + vectorFormatValues string = "Values" +) + +type VectorFP32 struct { + Val []byte +} + +func (v *VectorFP32) Value() []any { + return []any{vectorFormatFP32, v.Val} +} + +var _ Vector = (*VectorFP32)(nil) + +type VectorValues struct { + Val []float64 +} + +func (v *VectorValues) Value() []any { + res := make([]any, 2+len(v.Val)) + res[0] = vectorFormatValues + res[1] = len(v.Val) + for i, v := range v.Val { + res[2+i] = v + } + return res +} + +var _ Vector = (*VectorValues)(nil) + +type VectorRef struct { + Name string // the name of the referent vector +} + +func (v *VectorRef) Value() []any { + return []any{"ele", v.Name} +} + +var _ Vector = (*VectorRef)(nil) + +type VectorScore struct { + Name string + Score float64 +} + +// `VADD key (FP32 | VALUES num) vector element` +// note: the API is experimental and may be subject to change. +func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { + return c.VAddWithArgs(ctx, key, element, val, &VAddArgs{}) +} + +type VAddArgs struct { + // the REDUCE option must be passed immediately after the key + Reduce int64 + Cas bool + + // The NoQuant, Q8 and Bin options are mutually exclusive. + NoQuant bool + Q8 bool + Bin bool + + EF int64 + SetAttr string + M int64 +} + +func (v VAddArgs) reduce() int64 { + return v.Reduce +} + +func (v VAddArgs) appendArgs(args []any) []any { + if v.Cas { + args = append(args, "cas") + } + + if v.NoQuant { + args = append(args, "noquant") + } else if v.Q8 { + args = append(args, "q8") + } else if v.Bin { + args = append(args, "bin") + } + + if v.EF > 0 { + args = append(args, "ef", strconv.FormatInt(v.EF, 10)) + } + if len(v.SetAttr) > 0 { + args = append(args, "setattr", v.SetAttr) + } + if v.M > 0 { + args = append(args, "m", strconv.FormatInt(v.M, 10)) + } + return args +} + +// `VADD key [REDUCE dim] (FP32 | VALUES num) vector element [CAS] [NOQUANT | Q8 | BIN] [EF build-exploration-factor] [SETATTR attributes] [M numlinks]` +// note: the API is experimental and may be subject to change. +func (c cmdable) VAddWithArgs(ctx context.Context, key, element string, val Vector, addArgs *VAddArgs) *BoolCmd { + if addArgs == nil { + addArgs = &VAddArgs{} + } + args := []any{"vadd", key} + if addArgs.reduce() > 0 { + args = append(args, "reduce", addArgs.reduce()) + } + args = append(args, val.Value()...) + args = append(args, element) + args = addArgs.appendArgs(args) + cmd := NewBoolCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VCARD key` +// note: the API is experimental and may be subject to change. +func (c cmdable) VCard(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "vcard", key) + _ = c(ctx, cmd) + return cmd +} + +// `VDIM key` +// note: the API is experimental and may be subject to change. +func (c cmdable) VDim(ctx context.Context, key string) *IntCmd { + cmd := NewIntCmd(ctx, "vdim", key) + _ = c(ctx, cmd) + return cmd +} + +// `VEMB key element [RAW]` +// note: the API is experimental and may be subject to change. +func (c cmdable) VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd { + args := []any{"vemb", key, element} + if raw { + args = append(args, "raw") + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VGETATTR key element` +// note: the API is experimental and may be subject to change. +func (c cmdable) VGetAttr(ctx context.Context, key, element string) *StringCmd { + cmd := NewStringCmd(ctx, "vgetattr", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VINFO key` +// note: the API is experimental and may be subject to change. +func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "vinfo", key) + _ = c(ctx, cmd) + return cmd +} + +// `VLINKS key element` +// note: the API is experimental and may be subject to change. +func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "vlinks", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VLINKS key element WITHSCORES` +// note: the API is experimental and may be subject to change. +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd { + cmd := NewVectorInfoSliceCmd(ctx, "vlinks", key, element, "withscores") + _ = c(ctx, cmd) + return cmd +} + +// `VRANDMEMBER key` +// note: the API is experimental and may be subject to change. +func (c cmdable) VRandMember(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "vrandmember", key) + _ = c(ctx, cmd) + return cmd +} + +// `VRANDMEMBER key [count]` +// note: the API is experimental and may be subject to change. +func (c cmdable) VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "vrandmember", key, count) + _ = c(ctx, cmd) + return cmd +} + +// `VREM key element` +// note: the API is experimental and may be subject to change. +func (c cmdable) VRem(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vrem", key, element) + _ = c(ctx, cmd) + return cmd +} + +// `VSETATTR key element "{ JSON obj }"` +// The `attr` must be something that can be marshaled to JSON (using encoding/JSON) unless +// the argument is a string or []byte when we assume that it can be passed directly as JSON. +// +// note: the API is experimental and may be subject to change. +func (c cmdable) VSetAttr(ctx context.Context, key, element string, attr interface{}) *BoolCmd { + var attrStr string + var err error + switch v := attr.(type) { + case string: + attrStr = v + case []byte: + attrStr = string(v) + default: + var bytes []byte + bytes, err = json.Marshal(v) + if err != nil { + // If marshalling fails, create the command and set the error; this command won't be executed. + cmd := NewBoolCmd(ctx, "vsetattr", key, element, "") + cmd.SetErr(err) + return cmd + } + attrStr = string(bytes) + } + cmd := NewBoolCmd(ctx, "vsetattr", key, element, attrStr) + _ = c(ctx, cmd) + return cmd +} + +// `VClearAttributes` clear attributes on a vector set element. +// The implementation of `VClearAttributes` is execute command `VSETATTR key element ""`. +// note: the API is experimental and may be subject to change. +func (c cmdable) VClearAttributes(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vsetattr", key, element, "") + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element)` +// note: the API is experimental and may be subject to change. +func (c cmdable) VSim(ctx context.Context, key string, val Vector) *StringSliceCmd { + return c.VSimWithArgs(ctx, key, val, &VSimArgs{}) +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) WITHSCORES` +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd { + return c.VSimWithArgsWithScores(ctx, key, val, &VSimArgs{}) +} + +type VSimArgs struct { + Count int64 + EF int64 + Filter string + FilterEF int64 + Truth bool + NoThread bool + // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. + // Epsilon float64 +} + +func (v VSimArgs) appendArgs(args []any) []any { + if v.Count > 0 { + args = append(args, "count", v.Count) + } + if v.EF > 0 { + args = append(args, "ef", v.EF) + } + if len(v.Filter) > 0 { + args = append(args, "filter", v.Filter) + } + if v.FilterEF > 0 { + args = append(args, "filter-ef", v.FilterEF) + } + if v.Truth { + args = append(args, "truth") + } + if v.NoThread { + args = append(args, "nothread") + } + // if v.Epsilon > 0 { + // args = append(args, "Epsilon", v.Epsilon) + // } + return args +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = simArgs.appendArgs(args) + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withscores") + args = simArgs.appendArgs(args) + cmd := NewVectorInfoSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vectorset_commands_integration_test.go b/vectorset_commands_integration_test.go new file mode 100644 index 0000000000..147fb84c5e --- /dev/null +++ b/vectorset_commands_integration_test.go @@ -0,0 +1,326 @@ +package redis_test + +import ( + "context" + "fmt" + "math/rand" + "time" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" + "github.com/redis/go-redis/v9/internal/proto" +) + +func expectNil(err error) { + Expect(err).NotTo(HaveOccurred()) +} + +func expectTrue(t bool) { + expectEqual(t, true) +} + +func expectEqual[T any, U any](a T, b U) { + Expect(a).To(BeEquivalentTo(b)) +} + +func generateRandomVector(dim int) redis.VectorValues { + rand.Seed(time.Now().UnixNano()) + v := make([]float64, dim) + for i := range v { + v[i] = float64(rand.Intn(1000)) + rand.Float64() + } + return redis.VectorValues{Val: v} +} + +var _ = Describe("Redis VectorSet commands", Label("vectorset"), func() { + ctx := context.TODO() + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, + }) + } + + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol + + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client + + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } + }) + + It("basic", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") + vecName := "basic" + val := &redis.VectorValues{ + Val: []float64{1.5, 2.4, 3.3, 4.2}, + } + ok, err := client.VAdd(ctx, vecName, "k1", val).Result() + expectNil(err) + expectTrue(ok) + + fp32 := "\x8f\xc2\xf9\x3e\xcb\xbe\xe9\xbe\xb0\x1e\xca\x3f\x5e\x06\x9e\x3f" + val2 := &redis.VectorFP32{ + Val: []byte(fp32), + } + ok, err = client.VAdd(ctx, vecName, "k2", val2).Result() + expectNil(err) + expectTrue(ok) + + dim, err := client.VDim(ctx, vecName).Result() + expectNil(err) + expectEqual(dim, 4) + + count, err := client.VCard(ctx, vecName).Result() + expectNil(err) + expectEqual(count, 2) + + ok, err = client.VRem(ctx, vecName, "k1").Result() + expectNil(err) + expectTrue(ok) + + count, err = client.VCard(ctx, vecName).Result() + expectNil(err) + expectEqual(count, 1) + }) + + It("basic similarity", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") + vecName := "basic_similarity" + + ok, err := client.VAdd(ctx, vecName, "k1", &redis.VectorValues{ + Val: []float64{1, 0, 0, 0}, + }).Result() + expectNil(err) + expectTrue(ok) + ok, err = client.VAdd(ctx, vecName, "k2", &redis.VectorValues{ + Val: []float64{0.99, 0.01, 0, 0}, + }).Result() + expectNil(err) + expectTrue(ok) + ok, err = client.VAdd(ctx, vecName, "k3", &redis.VectorValues{ + Val: []float64{0.1, 1, -1, 0.5}, + }).Result() + expectNil(err) + expectTrue(ok) + + sim, err := client.VSimWithScores(ctx, vecName, &redis.VectorValues{ + Val: []float64{1, 0, 0, 0}, + }).Result() + expectNil(err) + expectEqual(len(sim), 3) + simMap := make(map[string]float64) + for _, vi := range sim { + simMap[vi.Name] = vi.Score + } + expectTrue(simMap["k1"] > 0.99) + expectTrue(simMap["k2"] > 0.99) + expectTrue(simMap["k3"] < 0.8) + }) + + It("dimension operation", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") + vecName := "dimension_op" + originalDim := 100 + reducedDim := 50 + + v1 := generateRandomVector(originalDim) + ok, err := client.VAddWithArgs(ctx, vecName, "k1", &v1, &redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectNil(err) + expectTrue(ok) + + info, err := client.VInfo(ctx, vecName).Result() + expectNil(err) + dim := info["vector-dim"].(int64) + oriDim := info["projection-input-dim"].(int64) + expectEqual(dim, reducedDim) + expectEqual(oriDim, originalDim) + + wrongDim := 80 + wrongV := generateRandomVector(wrongDim) + _, err = client.VAddWithArgs(ctx, vecName, "kw", &wrongV, &redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectTrue(err != nil) + + v2 := generateRandomVector(originalDim) + ok, err = client.VAddWithArgs(ctx, vecName, "k2", &v2, &redis.VAddArgs{ + Reduce: int64(reducedDim), + }).Result() + expectNil(err) + expectTrue(ok) + }) + + It("remove", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") + vecName := "remove" + v1 := generateRandomVector(5) + ok, err := client.VAdd(ctx, vecName, "k1", &v1).Result() + expectNil(err) + expectTrue(ok) + + exist, err := client.Exists(ctx, vecName).Result() + expectNil(err) + expectEqual(exist, 1) + + ok, err = client.VRem(ctx, vecName, "k1").Result() + expectNil(err) + expectTrue(ok) + + exist, err = client.Exists(ctx, vecName).Result() + expectNil(err) + expectEqual(exist, 0) + }) + + It("all operations", func() { + SkipBeforeRedisVersion(8.0, "Redis 8.0 introduces support for VectorSet") + vecName := "commands" + vals := []struct { + name string + v redis.VectorValues + attr string + }{ + { + name: "k0", + v: redis.VectorValues{Val: []float64{1, 0, 0, 0}}, + attr: `{"age": 25, "name": "Alice", "active": true, "scores": [85, 90, 95], "city": "New York"}`, + }, + { + name: "k1", + v: redis.VectorValues{Val: []float64{0, 1, 0, 0}}, + attr: `{"age": 30, "name": "Bob", "active": false, "scores": [70, 75, 80], "city": "Boston"}`, + }, + { + name: "k2", + v: redis.VectorValues{Val: []float64{0, 0, 1, 0}}, + attr: `{"age": 35, "name": "Charlie", "scores": [60, 65, 70], "city": "Seattle"}`, + }, + { + name: "k3", + v: redis.VectorValues{Val: []float64{0, 0, 0, 1}}, + }, + { + name: "k4", + v: redis.VectorValues{Val: []float64{0.5, 0.5, 0, 0}}, + attr: `invalid json`, + }, + } + + // If the key doesn't exist, return null error + _, err := client.VRandMember(ctx, vecName).Result() + expectEqual(err.Error(), proto.Nil.Error()) + + // If the key doesn't exist, return an empty array + res, err := client.VRandMemberCount(ctx, vecName, 3).Result() + expectNil(err) + expectEqual(len(res), 0) + + for _, v := range vals { + ok, err := client.VAdd(ctx, vecName, v.name, &v.v).Result() + expectNil(err) + expectTrue(ok) + if len(v.attr) > 0 { + ok, err = client.VSetAttr(ctx, vecName, v.name, v.attr).Result() + expectNil(err) + expectTrue(ok) + } + } + + // VGetAttr + attr, err := client.VGetAttr(ctx, vecName, vals[1].name).Result() + expectNil(err) + expectEqual(attr, vals[1].attr) + + // VRandMember + _, err = client.VRandMember(ctx, vecName).Result() + expectNil(err) + + res, err = client.VRandMemberCount(ctx, vecName, 3).Result() + expectNil(err) + expectEqual(len(res), 3) + + res, err = client.VRandMemberCount(ctx, vecName, 10).Result() + expectNil(err) + expectEqual(len(res), len(vals)) + + // test equality + sim, err := client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age == 25`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test greater than + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age > 25`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test less than or equal + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age <= 30`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test string equality + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.name == "Alice"`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test string inequality + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.name != "Alice"`, + }).Result() + expectNil(err) + expectEqual(len(sim), 2) + + // test bool + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.active`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test logical add + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age > 20 and .age < 30`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + + // test logical or + sim, err = client.VSimWithArgs(ctx, vecName, &vals[0].v, &redis.VSimArgs{ + Filter: `.age < 30 or .age > 35`, + }).Result() + expectNil(err) + expectEqual(len(sim), 1) + expectEqual(sim[0], vals[0].name) + }) + }) + } +}) diff --git a/vectorset_commands_test.go b/vectorset_commands_test.go new file mode 100644 index 0000000000..9dbc8a78f9 --- /dev/null +++ b/vectorset_commands_test.go @@ -0,0 +1,542 @@ +package redis + +import ( + "context" + "encoding/json" + "reflect" + "testing" +) + +func TestVectorFP32_Value(t *testing.T) { + v := &VectorFP32{Val: []byte{1, 2, 3}} + got := v.Value() + want := []any{"FP32", []byte{1, 2, 3}} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorFP32.Value() = %v, want %v", got, want) + } +} + +func TestVectorValues_Value(t *testing.T) { + v := &VectorValues{Val: []float64{1.1, 2.2}} + got := v.Value() + want := []any{"Values", 2, 1.1, 2.2} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorValues.Value() = %v, want %v", got, want) + } +} + +func TestVectorRef_Value(t *testing.T) { + v := &VectorRef{Name: "foo"} + got := v.Value() + want := []any{"ele", "foo"} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorRef.Value() = %v, want %v", got, want) + } +} + +func TestVAdd(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VAdd(context.Background(), "k", "e", vec) + cmd, ok := m.lastCmd.(*BoolCmd) + if !ok { + t.Fatalf("expected BoolCmd, got %T", m.lastCmd) + } + if cmd.args[0] != "vadd" || cmd.args[1] != "k" || cmd.args[len(cmd.args)-1] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVAddWithArgs_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 3, Cas: true, NoQuant: true, EF: 5, SetAttr: "attr", M: 2} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"reduce", "cas", "noquant", "ef", "setattr", "m"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} + +func TestVCard(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VCard(context.Background(), "k") + cmd := m.lastCmd.(*IntCmd) + if cmd.args[0] != "vcard" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVDim(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VDim(context.Background(), "k") + cmd := m.lastCmd.(*IntCmd) + if cmd.args[0] != "vdim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVEmb(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VEmb(context.Background(), "k", "e", true) + cmd := m.lastCmd.(*SliceCmd) + if cmd.args[0] != "vemb" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "raw" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVGetAttr(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VGetAttr(context.Background(), "k", "e") + cmd := m.lastCmd.(*StringCmd) + if cmd.args[0] != "vgetattr" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVInfo(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VInfo(context.Background(), "k") + cmd := m.lastCmd.(*MapStringInterfaceCmd) + if cmd.args[0] != "vinfo" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVLinks(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VLinks(context.Background(), "k", "e") + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vlinks" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVLinksWithScores(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VLinksWithScores(context.Background(), "k", "e") + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vlinks" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "withscores" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVRandMember(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRandMember(context.Background(), "k") + cmd := m.lastCmd.(*StringCmd) + if cmd.args[0] != "vrandmember" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVRandMemberCount(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRandMemberCount(context.Background(), "k", 5) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vrandmember" || cmd.args[1] != "k" || cmd.args[2] != 5 { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVRem(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VRem(context.Background(), "k", "e") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vrem" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSetAttr_String(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VSetAttr(context.Background(), "k", "e", "foo") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vsetattr" || cmd.args[1] != "k" || cmd.args[2] != "e" || cmd.args[3] != "foo" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSetAttr_Bytes(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VSetAttr(context.Background(), "k", "e", []byte("bar")) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[3] != "bar" { + t.Errorf("expected 'bar', got %v", cmd.args[3]) + } +} + +func TestVSetAttr_MarshalStruct(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + val := struct{ A int }{A: 1} + c.VSetAttr(context.Background(), "k", "e", val) + cmd := m.lastCmd.(*BoolCmd) + want, _ := json.Marshal(val) + if cmd.args[3] != string(want) { + t.Errorf("expected marshalled struct, got %v", cmd.args[3]) + } +} + +func TestVSetAttr_MarshalError(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + bad := func() {} + cmd := c.VSetAttr(context.Background(), "k", "e", bad) + if cmd.Err() == nil { + t.Error("expected error for non-marshallable value") + } +} + +func TestVClearAttributes(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VClearAttributes(context.Background(), "k", "e") + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vsetattr" || cmd.args[3] != "" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSim(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithScores(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithScores(context.Background(), "k", vec) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" || cmd.args[len(cmd.args)-1] != "withscores" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgs_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VSimArgs{Count: 2, EF: 3, Filter: "f", FilterEF: 4, Truth: true, NoThread: true} + c.VSimWithArgs(context.Background(), "k", vec, args) + cmd := m.lastCmd.(*StringSliceCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"count", "ef", "filter", "filter-ef", "truth", "nothread"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} + +func TestVSimWithArgsWithScores_AllOptions(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VSimArgs{Count: 2, EF: 3, Filter: "f", FilterEF: 4, Truth: true, NoThread: true} + c.VSimWithArgsWithScores(context.Background(), "k", vec, args) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + found := map[string]bool{} + for _, a := range cmd.args { + if s, ok := a.(string); ok { + found[s] = true + } + } + for _, want := range []string{"count", "ef", "filter", "filter-ef", "truth", "nothread", "withscores"} { + if !found[want] { + t.Errorf("missing arg: %s", want) + } + } +} + +// Additional tests for missing coverage + +func TestVectorValues_EmptySlice(t *testing.T) { + v := &VectorValues{Val: []float64{}} + got := v.Value() + want := []any{"Values", 0} + if !reflect.DeepEqual(got, want) { + t.Errorf("VectorValues.Value() with empty slice = %v, want %v", got, want) + } +} + +func TestVEmb_WithoutRaw(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + c.VEmb(context.Background(), "k", "e", false) + cmd := m.lastCmd.(*SliceCmd) + if cmd.args[0] != "vemb" || cmd.args[1] != "k" || cmd.args[2] != "e" { + t.Errorf("unexpected args: %v", cmd.args) + } + if len(cmd.args) != 3 { + t.Errorf("expected 3 args when raw=false, got %d", len(cmd.args)) + } +} + +func TestVAddWithArgs_Q8Option(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Q8: true} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "q8" { + found = true + break + } + } + if !found { + t.Error("missing q8 arg") + } +} + +func TestVAddWithArgs_BinOption(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Bin: true} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "bin" { + found = true + break + } + } + if !found { + t.Error("missing bin arg") + } +} + +func TestVAddWithArgs_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VAddWithArgs(context.Background(), "k", "e", vec, nil) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgs_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgs(context.Background(), "k", vec, nil) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } +} + +func TestVSimWithArgsWithScores_NilArgs(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgsWithScores(context.Background(), "k", vec, nil) + cmd := m.lastCmd.(*VectorScoreSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Should still have withscores + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "withscores" { + found = true + break + } + } + if !found { + t.Error("missing withscores arg") + } +} + +func TestVAdd_WithVectorFP32(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorFP32{Val: []byte{1, 2, 3, 4}} + c.VAdd(context.Background(), "k", "e", vec) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that FP32 format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "FP32" { + found = true + break + } + } + if !found { + t.Error("missing FP32 format in args") + } +} + +func TestVAdd_WithVectorRef(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorRef{Name: "ref-vector"} + c.VAdd(context.Background(), "k", "e", vec) + cmd := m.lastCmd.(*BoolCmd) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that ele format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "ele" { + found = true + break + } + } + if !found { + t.Error("missing ele format in args") + } +} + +func TestVSim_WithVectorFP32(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorFP32{Val: []byte{1, 2, 3, 4}} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that FP32 format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "FP32" { + found = true + break + } + } + if !found { + t.Error("missing FP32 format in args") + } +} + +func TestVSim_WithVectorRef(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorRef{Name: "ref-vector"} + c.VSim(context.Background(), "k", vec) + cmd := m.lastCmd.(*StringSliceCmd) + if cmd.args[0] != "vsim" || cmd.args[1] != "k" { + t.Errorf("unexpected args: %v", cmd.args) + } + // Check that ele format is used + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == "ele" { + found = true + break + } + } + if !found { + t.Error("missing ele format in args") + } +} + +func TestVAddWithArgs_ReduceOption(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 128} + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + // Check that reduce appears early in args (after key) + if cmd.args[0] != "vadd" || cmd.args[1] != "k" || cmd.args[2] != "reduce" { + t.Errorf("unexpected args order: %v", cmd.args) + } +} + +func TestVAddWithArgs_ZeroValues(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + args := &VAddArgs{Reduce: 0, EF: 0, M: 0} // Zero values should not appear in args + c.VAddWithArgs(context.Background(), "k", "e", vec, args) + cmd := m.lastCmd.(*BoolCmd) + // Check that zero values don't appear + for _, a := range cmd.args { + if s, ok := a.(string); ok { + if s == "reduce" || s == "ef" || s == "m" { + t.Errorf("zero value option should not appear in args: %s", s) + } + } + } +} + +func TestVSimArgs_IndividualOptions(t *testing.T) { + tests := []struct { + name string + args *VSimArgs + want string + }{ + {"Count", &VSimArgs{Count: 5}, "count"}, + {"EF", &VSimArgs{EF: 10}, "ef"}, + {"Filter", &VSimArgs{Filter: "test"}, "filter"}, + {"FilterEF", &VSimArgs{FilterEF: 15}, "filter-ef"}, + {"Truth", &VSimArgs{Truth: true}, "truth"}, + {"NoThread", &VSimArgs{NoThread: true}, "nothread"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mockCmdable{} + c := m.asCmdable() + vec := &VectorValues{Val: []float64{1, 2}} + c.VSimWithArgs(context.Background(), "k", vec, tt.args) + cmd := m.lastCmd.(*StringSliceCmd) + found := false + for _, a := range cmd.args { + if s, ok := a.(string); ok && s == tt.want { + found = true + break + } + } + if !found { + t.Errorf("missing arg: %s", tt.want) + } + }) + } +} From d5afd749a3db0ecb27fdb0d2d29f43c544ed4b6b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:25:35 +0100 Subject: [PATCH 188/230] DOC-5078 vector set examples (#3394) --- .github/workflows/doctests.yaml | 10 +- Makefile | 32 +- doctests/Makefile | 16 + doctests/main_test.go | 16 + doctests/vec_set_test.go | 546 ++++++++++++++++++++++++++++++++ 5 files changed, 612 insertions(+), 8 deletions(-) create mode 100644 doctests/Makefile create mode 100644 doctests/main_test.go create mode 100644 doctests/vec_set_test.go diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index 56f882a0e7..bef6ecf4ed 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -16,9 +16,11 @@ jobs: services: redis-stack: - image: redis/redis-stack-server:latest - options: >- - --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + image: redislabs/client-libs-test:8.0.1-pre + env: + TLS_ENABLED: no + REDIS_CLUSTER: no + PORT: 6379 ports: - 6379:6379 @@ -38,4 +40,4 @@ jobs: - name: Test doc examples working-directory: ./doctests - run: go test -v + run: make test diff --git a/Makefile b/Makefile index fc175f5f18..655f16f44f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,19 @@ docker.stop: test: $(MAKE) docker.start - $(MAKE) test.ci + @if [ -z "$(REDIS_VERSION)" ]; then \ + echo "REDIS_VERSION not set, running all tests"; \ + $(MAKE) test.ci; \ + else \ + MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ + if [ "$$MAJOR_VERSION" -ge 8 ]; then \ + echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ + $(MAKE) test.ci; \ + else \ + echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ + $(MAKE) test.ci.skip-vectorsets; \ + fi; \ + fi $(MAKE) docker.stop test.ci: @@ -17,15 +29,27 @@ test.ci: (cd "$${dir}" && \ go mod tidy -compat=1.18 && \ go vet && \ - go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race); \ + go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ + done + cd internal/customvet && go build . + go vet -vettool ./internal/customvet/customvet + +test.ci.skip-vectorsets: + set -e; for dir in $(GO_MOD_DIRS); do \ + echo "go test in $${dir} (skipping vector sets)"; \ + (cd "$${dir}" && \ + go mod tidy -compat=1.18 && \ + go vet && \ + go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ + -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet bench: - go test ./... -test.run=NONE -test.bench=. -test.benchmem + go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example -.PHONY: all test bench fmt +.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt build: go build . diff --git a/doctests/Makefile b/doctests/Makefile new file mode 100644 index 0000000000..be38de58c3 --- /dev/null +++ b/doctests/Makefile @@ -0,0 +1,16 @@ +test: + @if [ -z "$(REDIS_VERSION)" ]; then \ + echo "REDIS_VERSION not set, running all tests"; \ + go test -v ./...; \ + else \ + MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ + if [ "$$MAJOR_VERSION" -ge 8 ]; then \ + echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ + go test -v ./...; \ + else \ + echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ + go test -v ./... -run '^(?!.*(?:vectorset|ExampleClient_vectorset)).*$$'; \ + fi; \ + fi + +.PHONY: test \ No newline at end of file diff --git a/doctests/main_test.go b/doctests/main_test.go new file mode 100644 index 0000000000..c0e56285d8 --- /dev/null +++ b/doctests/main_test.go @@ -0,0 +1,16 @@ +package example_commands_test + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +var RedisVersion float64 + +func init() { + // read REDIS_VERSION from env + RedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv("REDIS_VERSION"), "\""), 64) + fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion) +} diff --git a/doctests/vec_set_test.go b/doctests/vec_set_test.go new file mode 100644 index 0000000000..00d58346a1 --- /dev/null +++ b/doctests/vec_set_test.go @@ -0,0 +1,546 @@ +// EXAMPLE: vecset_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_vectorset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + defer rdb.Close() + // REMOVE_START + rdb.Del(ctx, "points", "quantSetQ8", "quantSetNoQ", "quantSetBin", "setNotReduced", "setReduced") + // REMOVE_END + + // STEP_START vadd + res1, err := rdb.VAdd(ctx, "points", "pt:A", + &redis.VectorValues{Val: []float64{1.0, 1.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> true + + res2, err := rdb.VAdd(ctx, "points", "pt:B", + &redis.VectorValues{Val: []float64{-1.0, -1.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.VAdd(ctx, "points", "pt:C", + &redis.VectorValues{Val: []float64{-1.0, 1.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + res4, err := rdb.VAdd(ctx, "points", "pt:D", + &redis.VectorValues{Val: []float64{1.0, -1.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> true + + res5, err := rdb.VAdd(ctx, "points", "pt:E", + &redis.VectorValues{Val: []float64{1.0, 0.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> true + + res6, err := rdb.Type(ctx, "points").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> vectorset + // STEP_END + + // STEP_START vcardvdim + res7, err := rdb.VCard(ctx, "points").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 5 + + res8, err := rdb.VDim(ctx, "points").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 2 + // STEP_END + + // STEP_START vemb + res9, err := rdb.VEmb(ctx, "points", "pt:A", false).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [0.9999999403953552 0.9999999403953552] + + res10, err := rdb.VEmb(ctx, "points", "pt:B", false).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> [-0.9999999403953552 -0.9999999403953552] + + res11, err := rdb.VEmb(ctx, "points", "pt:C", false).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> [-0.9999999403953552 0.9999999403953552] + + res12, err := rdb.VEmb(ctx, "points", "pt:D", false).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> [0.9999999403953552 -0.9999999403953552] + + res13, err := rdb.VEmb(ctx, "points", "pt:E", false).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> [1 0] + // STEP_END + + // STEP_START attr + attrs := map[string]interface{}{ + "name": "Point A", + "description": "First point added", + } + + res14, err := rdb.VSetAttr(ctx, "points", "pt:A", attrs).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> true + + res15, err := rdb.VGetAttr(ctx, "points", "pt:A").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) + // >>> {"description":"First point added","name":"Point A"} + + res16, err := rdb.VClearAttributes(ctx, "points", "pt:A").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> true + + // `VGetAttr()` returns an error if the attribute doesn't exist. + _, err = rdb.VGetAttr(ctx, "points", "pt:A").Result() + + if err != nil { + fmt.Println(err) + } + // STEP_END + + // STEP_START vrem + res18, err := rdb.VAdd(ctx, "points", "pt:F", + &redis.VectorValues{Val: []float64{0.0, 0.0}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> true + + res19, err := rdb.VCard(ctx, "points").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> 6 + + res20, err := rdb.VRem(ctx, "points", "pt:F").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> true + + res21, err := rdb.VCard(ctx, "points").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> 5 + // STEP_END + + // STEP_START vsim_basic + res22, err := rdb.VSim(ctx, "points", + &redis.VectorValues{Val: []float64{0.9, 0.1}}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> [pt:E pt:A pt:D pt:C pt:B] + // STEP_END + + // STEP_START vsim_options + res23, err := rdb.VSimWithArgsWithScores( + ctx, + "points", + &redis.VectorRef{Name: "pt:A"}, + &redis.VSimArgs{Count: 4}, + ).Result() + + if err != nil { + panic(err) + } + + sort.Slice(res23, func(i, j int) bool { + return res23[i].Name < res23[j].Name + }) + + fmt.Println(res23) + // >>> [{pt:A 1} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}] + // STEP_END + + // STEP_START vsim_filter + // Set attributes for filtering + res24, err := rdb.VSetAttr(ctx, "points", "pt:A", + map[string]interface{}{ + "size": "large", + "price": 18.99, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) // >>> true + + res25, err := rdb.VSetAttr(ctx, "points", "pt:B", + map[string]interface{}{ + "size": "large", + "price": 35.99, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) // >>> true + + res26, err := rdb.VSetAttr(ctx, "points", "pt:C", + map[string]interface{}{ + "size": "large", + "price": 25.99, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res26) // >>> true + + res27, err := rdb.VSetAttr(ctx, "points", "pt:D", + map[string]interface{}{ + "size": "small", + "price": 21.00, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> true + + res28, err := rdb.VSetAttr(ctx, "points", "pt:E", + map[string]interface{}{ + "size": "small", + "price": 17.75, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res28) // >>> true + + // Return elements in order of distance from point A whose + // `size` attribute is `large`. + res29, err := rdb.VSimWithArgs(ctx, "points", + &redis.VectorRef{Name: "pt:A"}, + &redis.VSimArgs{Filter: `.size == "large"`}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res29) // >>> [pt:A pt:C pt:B] + + // Return elements in order of distance from point A whose size is + // `large` and whose price is greater than 20.00. + res30, err := rdb.VSimWithArgs(ctx, "points", + &redis.VectorRef{Name: "pt:A"}, + &redis.VSimArgs{Filter: `.size == "large" && .price > 20.00`}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res30) // >>> [pt:C pt:B] + // STEP_END + + // Output: + // true + // true + // true + // true + // true + // vectorset + // 5 + // 2 + // [0.9999999403953552 0.9999999403953552] + // [-0.9999999403953552 -0.9999999403953552] + // [-0.9999999403953552 0.9999999403953552] + // [0.9999999403953552 -0.9999999403953552] + // [1 0] + // true + // {"description":"First point added","name":"Point A"} + // true + // redis: nil + // true + // 6 + // true + // 5 + // [pt:E pt:A pt:D pt:C pt:B] + // [{pt:A 1} {pt:C 0.5} {pt:D 0.5} {pt:E 0.8535534143447876}] + // true + // true + // true + // true + // true + // [pt:A pt:C pt:B] + // [pt:C pt:B] +} + +func ExampleClient_vectorset_quantization() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + defer rdb.Close() + // REMOVE_START + rdb.Del(ctx, "quantSetQ8", "quantSetNoQ", "quantSetBin") + // REMOVE_END + + // STEP_START add_quant + // Add with Q8 quantization + vecQ := &redis.VectorValues{Val: []float64{1.262185, 1.958231}} + + res1, err := rdb.VAddWithArgs(ctx, "quantSetQ8", "quantElement", vecQ, + &redis.VAddArgs{ + Q8: true, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> true + + embQ8, err := rdb.VEmb(ctx, "quantSetQ8", "quantElement", false).Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Q8 embedding: %v\n", embQ8) + // >>> Q8 embedding: [1.2621850967407227 1.9582309722900391] + + // Add with NOQUANT option + res2, err := rdb.VAddWithArgs(ctx, "quantSetNoQ", "quantElement", vecQ, + &redis.VAddArgs{ + NoQuant: true, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + embNoQ, err := rdb.VEmb(ctx, "quantSetNoQ", "quantElement", false).Result() + + if err != nil { + panic(err) + } + + fmt.Printf("NOQUANT embedding: %v\n", embNoQ) + // >>> NOQUANT embedding: [1.262185 1.958231] + + // Add with BIN quantization + res3, err := rdb.VAddWithArgs(ctx, "quantSetBin", "quantElement", vecQ, + &redis.VAddArgs{ + Bin: true, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + embBin, err := rdb.VEmb(ctx, "quantSetBin", "quantElement", false).Result() + + if err != nil { + panic(err) + } + + fmt.Printf("BIN embedding: %v\n", embBin) + // >>> BIN embedding: [1 1] + // STEP_END + + // Output: + // true + // Q8 embedding: [1.2643694877624512 1.958230972290039] + // true + // NOQUANT embedding: [1.262184977531433 1.958230972290039] + // true + // BIN embedding: [1 1] +} + +func ExampleClient_vectorset_dimension_reduction() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + defer rdb.Close() + // REMOVE_START + rdb.Del(ctx, "setNotReduced", "setReduced") + // REMOVE_END + + // STEP_START add_reduce + // Create a vector with 300 dimensions + values := make([]float64, 300) + + for i := 0; i < 300; i++ { + values[i] = float64(i) / 299 + } + + vecLarge := &redis.VectorValues{Val: values} + + // Add without reduction + res1, err := rdb.VAdd(ctx, "setNotReduced", "element", vecLarge).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> true + + dim1, err := rdb.VDim(ctx, "setNotReduced").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Dimension without reduction: %d\n", dim1) + // >>> Dimension without reduction: 300 + + // Add with reduction to 100 dimensions + res2, err := rdb.VAddWithArgs(ctx, "setReduced", "element", vecLarge, + &redis.VAddArgs{ + Reduce: 100, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + dim2, err := rdb.VDim(ctx, "setReduced").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Dimension after reduction: %d\n", dim2) + // >>> Dimension after reduction: 100 + // STEP_END + + // Output: + // true + // Dimension without reduction: 300 + // true + // Dimension after reduction: 100 +} From c37906c89694f15754b70e775023d06817639e6b Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:30:30 +0300 Subject: [PATCH 189/230] chore(release): Update release notes and versions for v9.10.0 (#3395) --- RELEASE-NOTES.md | 26 ++++++++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 13 files changed, 42 insertions(+), 16 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 7b6ce5cd8a..f6a4abb921 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,31 @@ # Release Notes +# 9.10.0 (2025-06-06) + +## 🚀 Highlights + +`go-redis` now supports [vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/). This data type is marked +as "in preview" in Redis and its support in `go-redis` is marked as experimental. You can find examples in the documentation and +in the `doctests` folder. + +# Changes + +## 🚀 New Features + +- feat: support vectorset ([#3375](https://github.com/redis/go-redis/pull/3375)) + +## 🧰 Maintenance + +- Add the missing NewFloatSliceResult for testing ([#3393](https://github.com/redis/go-redis/pull/3393)) +- DOC-5078 vector set examples ([#3394](https://github.com/redis/go-redis/pull/3394)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@AndBobsYourUncle](https://github.com/AndBobsYourUncle), [@andy-stark-redis](https://github.com/andy-stark-redis), [@fukua95](https://github.com/fukua95) and [@ndyakov](https://github.com/ndyakov) + + + # 9.9.0 (2025-05-27) ## 🚀 Highlights diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 6a193952ab..eac5651aea 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.10.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 89af916aca..b0769c4806 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.9.0 +require github.com/redis/go-redis/v9 v9.10.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index d56b1608ea..ee6a219d8b 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.10.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index bec4100d87..e501e03e93 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.9.0 +require github.com/redis/go-redis/v9 v9.10.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 989f21af1b..97fc824e9f 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.9.0 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/extra/redisotel/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.10.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 392bab9763..48fdc4e44b 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.9.0 +require github.com/redis/go-redis/v9 v9.10.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index d56b1608ea..ee6a219d8b 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.10.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 66ce5aedc9..c06d98084f 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.10.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index ff3446beed..b86582fc75 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.10.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 639c333cd7..1e415da6a8 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.9.0 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.10.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index fc022923fa..e1b40f96a1 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.9.0 + github.com/redis/go-redis/v9 v9.10.0 ) require ( diff --git a/version.go b/version.go index 24a037f352..cbed8bd8d2 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.9.0" + return "9.10.0" } From d591c39a548490de81243c00b4ef45b64ce2a271 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 9 Jun 2025 16:03:18 +0800 Subject: [PATCH 190/230] fix: insert entry during iterating over a map (#3398) Signed-off-by: fukua95 --- command.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/command.go b/command.go index 56b2257214..652e241be1 100644 --- a/command.go +++ b/command.go @@ -3584,15 +3584,14 @@ func (c *cmdsInfoCache) Get(ctx context.Context) (map[string]*CommandInfo, error return err } + lowerCmds := make(map[string]*CommandInfo, len(cmds)) + // Extensions have cmd names in upper case. Convert them to lower case. for k, v := range cmds { - lower := internal.ToLower(k) - if lower != k { - cmds[lower] = v - } + lowerCmds[internal.ToLower(k)] = v } - c.cmds = cmds + c.cmds = lowerCmds return nil }) return c.cmds, err From 3c005629c0730514c83090ba42e4c2a346337403 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 9 Jun 2025 16:06:21 +0800 Subject: [PATCH 191/230] fix: check if the shard exists to avoid returning nil (#3396) Signed-off-by: fukua95 --- ring.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ring.go b/ring.go index 8a004b8c0e..4da0b21a6b 100644 --- a/ring.go +++ b/ring.go @@ -405,7 +405,12 @@ func (c *ringSharding) GetByName(shardName string) (*ringShard, error) { c.mu.RLock() defer c.mu.RUnlock() - return c.shards.m[shardName], nil + shard, ok := c.shards.m[shardName] + if !ok { + return nil, errors.New("redis: the shard is not in the ring") + } + + return shard, nil } func (c *ringSharding) Random() (*ringShard, error) { From 9310da80e9b53107f9a698cd055ff5fd40ed9d87 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Mon, 9 Jun 2025 16:59:58 +0800 Subject: [PATCH 192/230] perf: reduce unnecessary memory allocation (#3399) Signed-off-by: fukua95 Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- commands_test.go | 2 +- internal/pool/pool_test.go | 2 +- probabilistic.go | 60 ++++++++++---------------------------- set_commands.go | 11 ++++--- sortedset_commands.go | 11 ++++--- unit_test.go | 2 +- 6 files changed, 29 insertions(+), 59 deletions(-) diff --git a/commands_test.go b/commands_test.go index 5256a6fbfa..72b206943b 100644 --- a/commands_test.go +++ b/commands_test.go @@ -460,7 +460,7 @@ var _ = Describe("Commands", func() { } // read the defaults to set them back later - for setting, _ := range expected { + for setting := range expected { val, err := client.ConfigGet(ctx, setting).Result() Expect(err).NotTo(HaveOccurred()) defaults[setting] = val[setting] diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 0f366cc7de..05949e42fd 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -410,7 +410,7 @@ var _ = Describe("race", func() { _, err = p.Get(ctx) Expect(err).To(MatchError(pool.ErrPoolTimeout)) p.Put(ctx, conn) - conn, err = p.Get(ctx) + _, err = p.Get(ctx) Expect(err).NotTo(HaveOccurred()) stats = p.Stats() diff --git a/probabilistic.go b/probabilistic.go index 02ca263cbd..c26e7cadbd 100644 --- a/probabilistic.go +++ b/probabilistic.go @@ -1116,18 +1116,14 @@ func (c cmdable) TopKListWithCount(ctx context.Context, key string) *MapStringIn // Returns OK on success or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.add/ func (c cmdable) TDigestAdd(ctx context.Context, key string, elements ...float64) *StatusCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.ADD" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewStatusCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1138,18 +1134,14 @@ func (c cmdable) TDigestAdd(ctx context.Context, key string, elements ...float64 // Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.byrank/ func (c cmdable) TDigestByRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(rank)) + args := make([]interface{}, 2+len(rank)) args[0] = "TDIGEST.BYRANK" args[1] = key - // Convert uint slice to []interface{} - interfaceSlice := make([]interface{}, len(rank)) - for i, v := range rank { - interfaceSlice[i] = v + for i, r := range rank { + args[2+i] = r } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1160,18 +1152,14 @@ func (c cmdable) TDigestByRank(ctx context.Context, key string, rank ...uint64) // Returns an array of floats representing the values at the specified ranks or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.byrevrank/ func (c cmdable) TDigestByRevRank(ctx context.Context, key string, rank ...uint64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(rank)) + args := make([]interface{}, 2+len(rank)) args[0] = "TDIGEST.BYREVRANK" args[1] = key - // Convert uint slice to []interface{} - interfaceSlice := make([]interface{}, len(rank)) - for i, v := range rank { - interfaceSlice[i] = v + for i, r := range rank { + args[2+i] = r } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1182,18 +1170,14 @@ func (c cmdable) TDigestByRevRank(ctx context.Context, key string, rank ...uint6 // Returns an array of floats representing the CDF values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.cdf/ func (c cmdable) TDigestCDF(ctx context.Context, key string, elements ...float64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.CDF" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1376,18 +1360,14 @@ func (c cmdable) TDigestMin(ctx context.Context, key string) *FloatCmd { // Returns an array of floats representing the quantile values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.quantile/ func (c cmdable) TDigestQuantile(ctx context.Context, key string, elements ...float64) *FloatSliceCmd { - args := make([]interface{}, 2, 2+len(elements)) + args := make([]interface{}, 2+len(elements)) args[0] = "TDIGEST.QUANTILE" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(elements)) for i, v := range elements { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewFloatSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1398,18 +1378,14 @@ func (c cmdable) TDigestQuantile(ctx context.Context, key string, elements ...fl // Returns an array of integers representing the rank values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.rank/ func (c cmdable) TDigestRank(ctx context.Context, key string, values ...float64) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) + args := make([]interface{}, 2+len(values)) args[0] = "TDIGEST.RANK" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(values)) for i, v := range values { - interfaceSlice[i] = v + args[i+2] = v } - args = append(args, interfaceSlice...) - cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd @@ -1431,18 +1407,14 @@ func (c cmdable) TDigestReset(ctx context.Context, key string) *StatusCmd { // Returns an array of integers representing the reverse rank values for each element or an error if the operation could not be completed. // For more information - https://redis.io/commands/tdigest.revrank/ func (c cmdable) TDigestRevRank(ctx context.Context, key string, values ...float64) *IntSliceCmd { - args := make([]interface{}, 2, 2+len(values)) + args := make([]interface{}, 2+len(values)) args[0] = "TDIGEST.REVRANK" args[1] = key - // Convert floatSlice to []interface{} - interfaceSlice := make([]interface{}, len(values)) for i, v := range values { - interfaceSlice[i] = v + args[2+i] = v } - args = append(args, interfaceSlice...) - cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd diff --git a/set_commands.go b/set_commands.go index cef8ad6d8b..355f514a06 100644 --- a/set_commands.go +++ b/set_commands.go @@ -78,16 +78,15 @@ func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd { } func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd { - args := make([]interface{}, 4+len(keys)) + numKeys := len(keys) + args := make([]interface{}, 4+numKeys) args[0] = "sintercard" - numkeys := int64(0) + args[1] = numKeys for i, key := range keys { args[2+i] = key - numkeys++ } - args[1] = numkeys - args[2+numkeys] = "limit" - args[3+numkeys] = limit + args[2+numKeys] = "limit" + args[3+numKeys] = limit cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) return cmd diff --git a/sortedset_commands.go b/sortedset_commands.go index 6701402703..14b3585882 100644 --- a/sortedset_commands.go +++ b/sortedset_commands.go @@ -257,16 +257,15 @@ func (c cmdable) ZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd } func (c cmdable) ZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd { - args := make([]interface{}, 4+len(keys)) + numKeys := len(keys) + args := make([]interface{}, 4+numKeys) args[0] = "zintercard" - numkeys := int64(0) + args[1] = numKeys for i, key := range keys { args[2+i] = key - numkeys++ } - args[1] = numkeys - args[2+numkeys] = "limit" - args[3+numkeys] = limit + args[2+numKeys] = "limit" + args[3+numKeys] = limit cmd := NewIntCmd(ctx, args...) _ = c(ctx, cmd) return cmd diff --git a/unit_test.go b/unit_test.go index e4d0e7b57d..e3b4647934 100644 --- a/unit_test.go +++ b/unit_test.go @@ -11,7 +11,7 @@ type mockCmdable struct { returnErr error } -func (m *mockCmdable) call(ctx context.Context, cmd Cmder) error { +func (m *mockCmdable) call(_ context.Context, cmd Cmder) error { m.lastCmd = cmd if m.returnErr != nil { cmd.SetErr(m.returnErr) From d9a11ab04ea5eb354955b8d154c3a9e1cf2cf818 Mon Sep 17 00:00:00 2001 From: Amir Salehi <54236454+iamamirsalehi@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:53:58 +0330 Subject: [PATCH 193/230] test: refactor TestBasicCredentials using table-driven tests (#3406) * test: refactor TestBasicCredentials using table-driven tests * Included additional edge cases: - Empty passwords - Special characters - Long strings - Unicode characters --- auth/auth_test.go | 113 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/auth/auth_test.go b/auth/auth_test.go index be762a8545..73984e331b 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -2,6 +2,7 @@ package auth import ( "errors" + "strings" "sync" "testing" "time" @@ -179,36 +180,90 @@ func TestStreamingCredentialsProvider(t *testing.T) { } func TestBasicCredentials(t *testing.T) { - t.Run("basic auth", func(t *testing.T) { - creds := NewBasicCredentials("user1", "pass1") - username, password := creds.BasicAuth() - if username != "user1" { - t.Fatalf("expected username 'user1', got '%s'", username) - } - if password != "pass1" { - t.Fatalf("expected password 'pass1', got '%s'", password) - } - }) - - t.Run("raw credentials", func(t *testing.T) { - creds := NewBasicCredentials("user1", "pass1") - raw := creds.RawCredentials() - expected := "user1:pass1" - if raw != expected { - t.Fatalf("expected raw credentials '%s', got '%s'", expected, raw) - } - }) + tests := []struct { + name string + username string + password string + expectedUser string + expectedPass string + expectedRaw string + }{ + { + name: "basic auth", + username: "user1", + password: "pass1", + expectedUser: "user1", + expectedPass: "pass1", + expectedRaw: "user1:pass1", + }, + { + name: "empty username", + username: "", + password: "pass1", + expectedUser: "", + expectedPass: "pass1", + expectedRaw: ":pass1", + }, + { + name: "empty password", + username: "user1", + password: "", + expectedUser: "user1", + expectedPass: "", + expectedRaw: "user1:", + }, + { + name: "both username and password empty", + username: "", + password: "", + expectedUser: "", + expectedPass: "", + expectedRaw: ":", + }, + { + name: "special characters", + username: "user:1", + password: "pa:ss@!#", + expectedUser: "user:1", + expectedPass: "pa:ss@!#", + expectedRaw: "user:1:pa:ss@!#", + }, + { + name: "unicode characters", + username: "ユーザー", + password: "密碼123", + expectedUser: "ユーザー", + expectedPass: "密碼123", + expectedRaw: "ユーザー:密碼123", + }, + { + name: "long credentials", + username: strings.Repeat("u", 1000), + password: strings.Repeat("p", 1000), + expectedUser: strings.Repeat("u", 1000), + expectedPass: strings.Repeat("p", 1000), + expectedRaw: strings.Repeat("u", 1000) + ":" + strings.Repeat("p", 1000), + }, + } - t.Run("empty username", func(t *testing.T) { - creds := NewBasicCredentials("", "pass1") - username, password := creds.BasicAuth() - if username != "" { - t.Fatalf("expected empty username, got '%s'", username) - } - if password != "pass1" { - t.Fatalf("expected password 'pass1', got '%s'", password) - } - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creds := NewBasicCredentials(tt.username, tt.password) + + user, pass := creds.BasicAuth() + if user != tt.expectedUser { + t.Errorf("BasicAuth() username = %q; want %q", user, tt.expectedUser) + } + if pass != tt.expectedPass { + t.Errorf("BasicAuth() password = %q; want %q", pass, tt.expectedPass) + } + + raw := creds.RawCredentials() + if raw != tt.expectedRaw { + t.Errorf("RawCredentials() = %q; want %q", raw, tt.expectedRaw) + } + }) + } } func TestReAuthCredentialsListener(t *testing.T) { From 5920b3be74628644fdf5d492f607509f29647e95 Mon Sep 17 00:00:00 2001 From: cxljs Date: Mon, 16 Jun 2025 21:55:23 +0800 Subject: [PATCH 194/230] chore: remove a redundant method (#3401) Signed-off-by: fukua95 Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- osscluster.go | 7 ------- osscluster_test.go | 6 ++++++ redis.go | 7 ------- redis_test.go | 6 ++++++ ring.go | 7 ------- ring_test.go | 6 ++++++ 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/osscluster.go b/osscluster.go index 6c6b756380..a68f7eab17 100644 --- a/osscluster.go +++ b/osscluster.go @@ -979,13 +979,6 @@ func (c *ClusterClient) Close() error { return c.nodes.Close() } -// Do create a Cmd from the args and processes the cmd. -func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) diff --git a/osscluster_test.go b/osscluster_test.go index 6e214a7191..993411837e 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -264,6 +264,12 @@ var _ = Describe("ClusterClient", func() { var client *redis.ClusterClient assertClusterClient := func() { + It("do", func() { + val, err := client.Do(ctx, "ping").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("PONG")) + }) + It("should GET/SET/DEL", func() { err := client.Get(ctx, "A").Err() Expect(err).To(Equal(redis.Nil)) diff --git a/redis.go b/redis.go index bafe82f752..a368623aa0 100644 --- a/redis.go +++ b/redis.go @@ -776,13 +776,6 @@ func (c *Client) Conn() *Conn { return newConn(c.opt, pool.NewStickyConnPool(c.connPool), &c.hooksMixin) } -// Do create a Cmd from the args and processes the cmd. -func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *Client) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) diff --git a/redis_test.go b/redis_test.go index dd14214f1a..6aaa0a7547 100644 --- a/redis_test.go +++ b/redis_test.go @@ -89,6 +89,12 @@ var _ = Describe("Client", func() { Expect(err).NotTo(HaveOccurred()) }) + It("do", func() { + val, err := client.Do(ctx, "ping").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("PONG")) + }) + It("should ping", func() { val, err := client.Ping(ctx).Result() Expect(err).NotTo(HaveOccurred()) diff --git a/ring.go b/ring.go index 4da0b21a6b..1f75913a3e 100644 --- a/ring.go +++ b/ring.go @@ -563,13 +563,6 @@ func (c *Ring) SetAddrs(addrs map[string]string) { c.sharding.SetAddrs(addrs) } -// Do create a Cmd from the args and processes the cmd. -func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd { - cmd := NewCmd(ctx, args...) - _ = c.Process(ctx, cmd) - return cmd -} - func (c *Ring) Process(ctx context.Context, cmd Cmder) error { err := c.processHook(ctx, cmd) cmd.SetErr(err) diff --git a/ring_test.go b/ring_test.go index aaac74dc9d..ef95e9805d 100644 --- a/ring_test.go +++ b/ring_test.go @@ -74,6 +74,12 @@ var _ = Describe("Redis Ring", func() { Expect(ring.Close()).NotTo(HaveOccurred()) }) + It("do", func() { + val, err := ring.Do(ctx, "ping").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("PONG")) + }) + It("supports context", func() { ctx, cancel := context.WithCancel(ctx) cancel() From 82751d637d54ac9ed6beb6213f6648fd2c25f311 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:28:58 +0300 Subject: [PATCH 195/230] chore(ci): update CI redis builds (#3407) --- .github/actions/run-tests/action.yml | 6 +++--- .github/workflows/build.yml | 14 +++++++------- .github/workflows/doctests.yaml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 0696f38de6..e9bb7e797b 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,9 +25,9 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0.1"]="8.0.1-pre" - ["7.4.2"]="rs-7.4.0-v2" - ["7.2.7"]="rs-7.2.0-v14" + ["8.0.x"]="8.0.2" + ["7.4.x"]="rs-7.4.0-v5" + ["7.2.x"]="rs-7.2.0-v17" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bde6cc721d..c8b3d1b686 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,8 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0.1" # 8.0.1 - - "7.4.2" # should use redis stack 7.4 + - "8.0.x" # Redis CE 8.0 + - "7.4.x" # Redis stack 7.4 go-version: - "1.23.x" - "1.24.x" @@ -43,8 +43,8 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0.1"]="8.0.1-pre" - ["7.4.2"]="rs-7.4.0-v2" + ["8.0.x"]="8.0.2" + ["7.4.x"]="rs-7.4.0-v5" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV @@ -72,9 +72,9 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0.1" # 8.0.1 - - "7.4.2" # should use redis stack 7.4 - - "7.2.7" # should redis stack 7.2 + - "8.0.x" # Redis CE 8.0 + - "7.4.x" # Redis stack 7.4 + - "7.2.x" # Redis stack 7.2 go-version: - "1.23.x" - "1.24.x" diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index bef6ecf4ed..321654fad4 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -16,7 +16,7 @@ jobs: services: redis-stack: - image: redislabs/client-libs-test:8.0.1-pre + image: redislabs/client-libs-test:8.0.2 env: TLS_ENABLED: no REDIS_CLUSTER: no From 8319098e54cad897d2fe87ddf7f2b10c44e63bfe Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:18:00 +0300 Subject: [PATCH 196/230] fix(txpipeline): should return error on multi/exec on multiple slots [CAE-1028] (#3408) * fix(txpipeline): should return error on multi/exec on multiple slots * fix(txpipeline): test normal tx pipeline behaviour * chore(err): Extract crossslot err and add test * fix(txpipeline): short curcuit the tx if there are no commands * chore(tests): validate keys are in different slots --- error.go | 6 ++++++ osscluster.go | 10 ++++++++++ osscluster_test.go | 49 +++++++++++++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/error.go b/error.go index 8c811966fb..8013de44a1 100644 --- a/error.go +++ b/error.go @@ -22,6 +22,12 @@ var ErrPoolExhausted = pool.ErrPoolExhausted // ErrPoolTimeout timed out waiting to get a connection from the connection pool. var ErrPoolTimeout = pool.ErrPoolTimeout +// ErrCrossSlot is returned when keys are used in the same Redis command and +// the keys are not in the same hash slot. This error is returned by Redis +// Cluster and will be returned by the client when TxPipeline or TxPipelined +// is used on a ClusterClient with keys in different slots. +var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot") + // HasErrorPrefix checks if the err is a Redis error and the message contains a prefix. func HasErrorPrefix(err error, prefix string) bool { var rErr Error diff --git a/osscluster.go b/osscluster.go index a68f7eab17..0dce50a4ac 100644 --- a/osscluster.go +++ b/osscluster.go @@ -1497,6 +1497,10 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err // Trim multi .. exec. cmds = cmds[1 : len(cmds)-1] + if len(cmds) == 0 { + return nil + } + state, err := c.state.Get(ctx) if err != nil { setCmdsErr(cmds, err) @@ -1504,6 +1508,12 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err } cmdsMap := c.mapCmdsBySlot(cmds) + // TxPipeline does not support cross slot transaction. + if len(cmdsMap) > 1 { + setCmdsErr(cmds, ErrCrossSlot) + return ErrCrossSlot + } + for slot, cmds := range cmdsMap { node, err := state.slotMasterNode(slot) if err != nil { diff --git a/osscluster_test.go b/osscluster_test.go index 993411837e..10023218d1 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -462,8 +462,7 @@ var _ = Describe("ClusterClient", func() { Describe("pipelining", func() { var pipe *redis.Pipeline - assertPipeline := func() { - keys := []string{"A", "B", "C", "D", "E", "F", "G"} + assertPipeline := func(keys []string) { It("follows redirects", func() { if !failover { @@ -482,13 +481,12 @@ var _ = Describe("ClusterClient", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmds).To(HaveLen(14)) - _ = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error { - defer GinkgoRecover() - Eventually(func() int64 { - return node.DBSize(ctx).Val() - }, 30*time.Second).ShouldNot(BeZero()) - return nil - }) + // Check that all keys are set. + for _, key := range keys { + Eventually(func() string { + return client.Get(ctx, key).Val() + }, 30*time.Second).Should(Equal(key + "_value")) + } if !failover { for _, key := range keys { @@ -517,14 +515,14 @@ var _ = Describe("ClusterClient", func() { }) It("works with missing keys", func() { - pipe.Set(ctx, "A", "A_value", 0) - pipe.Set(ctx, "C", "C_value", 0) + pipe.Set(ctx, "A{s}", "A_value", 0) + pipe.Set(ctx, "C{s}", "C_value", 0) _, err := pipe.Exec(ctx) Expect(err).NotTo(HaveOccurred()) - a := pipe.Get(ctx, "A") - b := pipe.Get(ctx, "B") - c := pipe.Get(ctx, "C") + a := pipe.Get(ctx, "A{s}") + b := pipe.Get(ctx, "B{s}") + c := pipe.Get(ctx, "C{s}") cmds, err := pipe.Exec(ctx) Expect(err).To(Equal(redis.Nil)) Expect(cmds).To(HaveLen(3)) @@ -547,7 +545,8 @@ var _ = Describe("ClusterClient", func() { AfterEach(func() {}) - assertPipeline() + keys := []string{"A", "B", "C", "D", "E", "F", "G"} + assertPipeline(keys) It("doesn't fail node with context.Canceled error", func() { ctx, cancel := context.WithCancel(context.Background()) @@ -590,7 +589,25 @@ var _ = Describe("ClusterClient", func() { AfterEach(func() {}) - assertPipeline() + // TxPipeline doesn't support cross slot commands. + // Use hashtag to force all keys to the same slot. + keys := []string{"A{s}", "B{s}", "C{s}", "D{s}", "E{s}", "F{s}", "G{s}"} + assertPipeline(keys) + + // make sure CrossSlot error is returned + It("returns CrossSlot error", func() { + pipe.Set(ctx, "A{s}", "A_value", 0) + pipe.Set(ctx, "B{t}", "B_value", 0) + Expect(hashtag.Slot("A{s}")).NotTo(Equal(hashtag.Slot("B{t}"))) + _, err := pipe.Exec(ctx) + Expect(err).To(MatchError(redis.ErrCrossSlot)) + }) + + // doesn't fail when no commands are queued + It("returns no error when there are no commands", func() { + _, err := pipe.Exec(ctx) + Expect(err).NotTo(HaveOccurred()) + }) }) }) From 61200f7c6b62b7dd2aabbbae28437ba54ee96bd9 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:18:51 +0300 Subject: [PATCH 197/230] [CAE-1046] fix(loading): cache the loaded flag for slave nodes (#3410) * fix(loading): cache the loaded flag for slave nodes * fix(lint): make linter happy --- osscluster.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osscluster.go b/osscluster.go index 0dce50a4ac..55017d8ba3 100644 --- a/osscluster.go +++ b/osscluster.go @@ -340,6 +340,7 @@ type clusterNode struct { latency uint32 // atomic generation uint32 // atomic failing uint32 // atomic + loaded uint32 // atomic // last time the latency measurement was performed for the node, stored in nanoseconds // from epoch @@ -406,6 +407,7 @@ func (n *clusterNode) Latency() time.Duration { func (n *clusterNode) MarkAsFailing() { atomic.StoreUint32(&n.failing, uint32(time.Now().Unix())) + atomic.StoreUint32(&n.loaded, 0) } func (n *clusterNode) Failing() bool { @@ -449,11 +451,21 @@ func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) { } func (n *clusterNode) Loading() bool { + loaded := atomic.LoadUint32(&n.loaded) + if loaded == 1 { + return false + } + + // check if the node is loading ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() err := n.Client.Ping(ctx).Err() - return err != nil && isLoadingError(err) + loading := err != nil && isLoadingError(err) + if !loading { + atomic.StoreUint32(&n.loaded, 1) + } + return loading } //------------------------------------------------------------------------------ From 4de2ffe46bab74dc55abb6414669dda9e0ec404d Mon Sep 17 00:00:00 2001 From: WeizhongTu Date: Fri, 20 Jun 2025 17:07:14 +0800 Subject: [PATCH 198/230] feat: optimize connection pool waitTurn (#3412) --- internal/pool/pool.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/pool/pool.go b/internal/pool/pool.go index e7d951e268..3ee3dea6d8 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -325,6 +325,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { start := time.Now() timer := timers.Get().(*time.Timer) + defer timers.Put(timer) timer.Reset(p.cfg.PoolTimeout) select { @@ -332,7 +333,6 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { if !timer.Stop() { <-timer.C } - timers.Put(timer) return ctx.Err() case p.queue <- struct{}{}: p.waitDurationNs.Add(time.Since(start).Nanoseconds()) @@ -340,10 +340,8 @@ func (p *ConnPool) waitTurn(ctx context.Context) error { if !timer.Stop() { <-timer.C } - timers.Put(timer) return nil case <-timer.C: - timers.Put(timer) atomic.AddUint32(&p.stats.Timeouts, 1) return ErrPoolTimeout } From ff78daa42e90c5a0489185ff9bc70e17d1ea1c8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:49:12 +0300 Subject: [PATCH 199/230] chore(deps): bump rojopolis/spellcheck-github-actions (#3414) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.49.0 to 0.51.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.49.0...0.51.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 6ab2c46701..81e73cd4ba 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.49.0 + uses: rojopolis/spellcheck-github-actions@0.51.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 1e38046c7c83e8b6236e90ef875f38e92ffdddb3 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 23 Jun 2025 09:49:36 +0100 Subject: [PATCH 200/230] DOC-5229 probabilistic data type examples (#3413) * DOC-5078 added basic vector set examples * DOC-5078 formatting and extra comments * DOC-5078 fixed nondeterministic test * wip(tests): run doctests for vector sets on redis 8 only * DOC-5229 added probabilistic data type examples --------- Co-authored-by: Nedyalko Dyakov --- doctests/home_prob_dts_test.go | 412 +++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 doctests/home_prob_dts_test.go diff --git a/doctests/home_prob_dts_test.go b/doctests/home_prob_dts_test.go new file mode 100644 index 0000000000..dcbbfcfb88 --- /dev/null +++ b/doctests/home_prob_dts_test.go @@ -0,0 +1,412 @@ +// EXAMPLE: home_prob_dts +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_probabilistic_datatypes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + rdb.Del(ctx, + "recorded_users", "other_users", + "group:1", "group:2", "both_groups", + "items_sold", + "male_heights", "female_heights", "all_heights", + "top_3_songs") + // REMOVE_END + + // STEP_START bloom + res1, err := rdb.BFMAdd( + ctx, + "recorded_users", + "andy", "cameron", "david", "michelle", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> [true true true true] + + res2, err := rdb.BFExists(ctx, + "recorded_users", "cameron", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.BFExists(ctx, "recorded_users", "kaitlyn").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> false + // STEP_END + + // STEP_START cuckoo + res4, err := rdb.CFAdd(ctx, "other_users", "paolo").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> true + + res5, err := rdb.CFAdd(ctx, "other_users", "kaitlyn").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> true + + res6, err := rdb.CFAdd(ctx, "other_users", "rachel").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> true + + res7, err := rdb.CFMExists(ctx, + "other_users", "paolo", "rachel", "andy", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> [true true false] + + res8, err := rdb.CFDel(ctx, "other_users", "paolo").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> true + + res9, err := rdb.CFExists(ctx, "other_users", "paolo").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> false + // STEP_END + + // STEP_START hyperloglog + res10, err := rdb.PFAdd( + ctx, + "group:1", + "andy", "cameron", "david", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 1 + + res11, err := rdb.PFCount(ctx, "group:1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 3 + + res12, err := rdb.PFAdd(ctx, + "group:2", + "kaitlyn", "michelle", "paolo", "rachel", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> 1 + + res13, err := rdb.PFCount(ctx, "group:2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> 4 + + res14, err := rdb.PFMerge( + ctx, + "both_groups", + "group:1", "group:2", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> OK + + res15, err := rdb.PFCount(ctx, "both_groups").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> 7 + // STEP_END + + // STEP_START cms + // Specify that you want to keep the counts within 0.01 + // (0.1%) of the true value with a 0.005 (0.05%) chance + // of going outside this limit. + res16, err := rdb.CMSInitByProb(ctx, "items_sold", 0.01, 0.005).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> OK + + // The parameters for `CMSIncrBy()` are two lists. The count + // for each item in the first list is incremented by the + // value at the same index in the second list. + res17, err := rdb.CMSIncrBy(ctx, "items_sold", + "bread", 300, + "tea", 200, + "coffee", 200, + "beer", 100, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [300 200 200 100] + + res18, err := rdb.CMSIncrBy(ctx, "items_sold", + "bread", 100, + "coffee", 150, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> [400 350] + + res19, err := rdb.CMSQuery(ctx, + "items_sold", + "bread", "tea", "coffee", "beer", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> [400 200 350 100] + // STEP_END + + // STEP_START tdigest + res20, err := rdb.TDigestCreate(ctx, "male_heights").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> OK + + res21, err := rdb.TDigestAdd(ctx, "male_heights", + 175.5, 181, 160.8, 152, 177, 196, 164, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> OK + + res22, err := rdb.TDigestMin(ctx, "male_heights").Result() + if err != nil { + panic(err) + } + fmt.Println(res22) // >>> 152 + + res23, err := rdb.TDigestMax(ctx, "male_heights").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> 196 + + res24, err := rdb.TDigestQuantile(ctx, "male_heights", 0.75).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) // >>> [181] + + // Note that the CDF value for 181 is not exactly + // 0.75. Both values are estimates. + res25, err := rdb.TDigestCDF(ctx, "male_heights", 181).Result() + + if err != nil { + panic(err) + } + + fmt.Printf("%.4f\n", res25[0]) // >>> 0.7857 + + res26, err := rdb.TDigestCreate(ctx, "female_heights").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res26) // >>> OK + + res27, err := rdb.TDigestAdd(ctx, "female_heights", + 155.5, 161, 168.5, 170, 157.5, 163, 171, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> OK + + res28, err := rdb.TDigestQuantile(ctx, "female_heights", 0.75).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res28) // >>> [170] + + res29, err := rdb.TDigestMerge(ctx, "all_heights", + nil, + "male_heights", "female_heights", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res29) // >>> OK + + res30, err := rdb.TDigestQuantile(ctx, "all_heights", 0.75).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res30) // >>> [175.5] + // STEP_END + + // STEP_START topk + // Create a TopK filter that keeps track of the top 3 items + res31, err := rdb.TopKReserve(ctx, "top_3_songs", 3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res31) // >>> OK + + // Add some items to the filter + res32, err := rdb.TopKIncrBy(ctx, + "top_3_songs", + "Starfish Trooper", 3000, + "Only one more time", 1850, + "Rock me, Handel", 1325, + "How will anyone know?", 3890, + "Average lover", 4098, + "Road to everywhere", 770, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res32) + // >>> [ Rock me, Handel Only one more time ] + + res33, err := rdb.TopKList(ctx, "top_3_songs").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res33) + // >>> [Average lover How will anyone know? Starfish Trooper] + + // Query the count for specific items + res34, err := rdb.TopKQuery( + ctx, + "top_3_songs", + "Starfish Trooper", "Road to everywhere", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res34) // >>> [true false] + // STEP_END + + // Output: + // [true true true true] + // true + // false + // true + // true + // true + // [true true false] + // true + // false + // 1 + // 3 + // 1 + // 4 + // OK + // 7 + // OK + // [300 200 200 100] + // [400 350] + // [400 200 350 100] + // OK + // OK + // 152 + // 196 + // [181] + // 0.7857 + // OK + // OK + // [170] + // OK + // [175.5] + // OK + // [ Rock me, Handel Only one more time ] + // [Average lover How will anyone know? Starfish Trooper] + // [true false] +} From d2bd31ce8ab69ba42edccd4078d48647730daf3c Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:34:23 +0300 Subject: [PATCH 201/230] fix(txpipeline): keyless commands should take the slot of the keyed (#3411) * fix(txpipeline): keyless commands should take the slot of the keyed commands * fix(txpipeline): extract only keyed cmds from all cmds * chore(test): Add tests for keyless cmds and txpipeline * fix(cmdSlot): Add preferred random slot * fix(cmdSlot): Add shortlist of keyless cmds * chore(test): Fix ring test * fix(keylessCommands): Add list of keyless commands Add list of keyless Commands based on the Commands output for redis 8 * chore(txPipeline): refactor slottedCommands impl * fix(osscluster): typo --- command.go | 61 +++++++++++++++++++++++- internal_test.go | 11 ++++- osscluster.go | 112 ++++++++++++++++++++++++++++----------------- osscluster_test.go | 9 ++++ ring_test.go | 12 ++--- 5 files changed, 155 insertions(+), 50 deletions(-) diff --git a/command.go b/command.go index 652e241be1..b79338cb92 100644 --- a/command.go +++ b/command.go @@ -17,6 +17,55 @@ import ( "github.com/redis/go-redis/v9/internal/util" ) +// keylessCommands contains Redis commands that have empty key specifications (9th slot empty) +// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands +var keylessCommands = map[string]struct{}{ + "acl": {}, + "asking": {}, + "auth": {}, + "bgrewriteaof": {}, + "bgsave": {}, + "client": {}, + "cluster": {}, + "config": {}, + "debug": {}, + "discard": {}, + "echo": {}, + "exec": {}, + "failover": {}, + "function": {}, + "hello": {}, + "latency": {}, + "lolwut": {}, + "module": {}, + "monitor": {}, + "multi": {}, + "pfselftest": {}, + "ping": {}, + "psubscribe": {}, + "psync": {}, + "publish": {}, + "pubsub": {}, + "punsubscribe": {}, + "quit": {}, + "readonly": {}, + "readwrite": {}, + "replconf": {}, + "replicaof": {}, + "role": {}, + "save": {}, + "script": {}, + "select": {}, + "shutdown": {}, + "slaveof": {}, + "slowlog": {}, + "subscribe": {}, + "swapdb": {}, + "sync": {}, + "unsubscribe": {}, + "unwatch": {}, +} + type Cmder interface { // command name. // e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster". @@ -75,12 +124,22 @@ func writeCmd(wr *proto.Writer, cmd Cmder) error { return wr.WriteArgs(cmd.Args()) } +// cmdFirstKeyPos returns the position of the first key in the command's arguments. +// If the command does not have a key, it returns 0. +// TODO: Use the data in CommandInfo to determine the first key position. func cmdFirstKeyPos(cmd Cmder) int { if pos := cmd.firstKeyPos(); pos != 0 { return int(pos) } - switch cmd.Name() { + name := cmd.Name() + + // first check if the command is keyless + if _, ok := keylessCommands[name]; ok { + return 0 + } + + switch name { case "eval", "evalsha", "eval_ro", "evalsha_ro": if cmd.stringArg(2) != "0" { return 3 diff --git a/internal_test.go b/internal_test.go index 8f1f1f3121..4a655cff0a 100644 --- a/internal_test.go +++ b/internal_test.go @@ -364,15 +364,22 @@ var _ = Describe("ClusterClient", func() { It("select slot from args for GETKEYSINSLOT command", func() { cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200) - slot := client.cmdSlot(cmd) + slot := client.cmdSlot(cmd, -1) Expect(slot).To(Equal(100)) }) It("select slot from args for COUNTKEYSINSLOT command", func() { cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100) - slot := client.cmdSlot(cmd) + slot := client.cmdSlot(cmd, -1) Expect(slot).To(Equal(100)) }) + + It("follows preferred random slot", func() { + cmd := NewStatusCmd(ctx, "ping") + + slot := client.cmdSlot(cmd, 101) + Expect(slot).To(Equal(101)) + }) }) }) diff --git a/osscluster.go b/osscluster.go index 55017d8ba3..0526022ba0 100644 --- a/osscluster.go +++ b/osscluster.go @@ -998,7 +998,7 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error { } func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { - slot := c.cmdSlot(cmd) + slot := c.cmdSlot(cmd, -1) var node *clusterNode var moved bool var ask bool @@ -1344,9 +1344,13 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd return err } + preferredRandomSlot := -1 if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) { for _, cmd := range cmds { - slot := c.cmdSlot(cmd) + slot := c.cmdSlot(cmd, preferredRandomSlot) + if preferredRandomSlot == -1 { + preferredRandomSlot = slot + } node, err := c.slotReadOnlyNode(state, slot) if err != nil { return err @@ -1357,7 +1361,10 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd } for _, cmd := range cmds { - slot := c.cmdSlot(cmd) + slot := c.cmdSlot(cmd, preferredRandomSlot) + if preferredRandomSlot == -1 { + preferredRandomSlot = slot + } node, err := state.slotMasterNode(slot) if err != nil { return err @@ -1519,58 +1526,78 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err return err } - cmdsMap := c.mapCmdsBySlot(cmds) - // TxPipeline does not support cross slot transaction. - if len(cmdsMap) > 1 { + keyedCmdsBySlot := c.slottedKeyedCommands(cmds) + slot := -1 + switch len(keyedCmdsBySlot) { + case 0: + slot = hashtag.RandomSlot() + case 1: + for sl := range keyedCmdsBySlot { + slot = sl + break + } + default: + // TxPipeline does not support cross slot transaction. setCmdsErr(cmds, ErrCrossSlot) return ErrCrossSlot } - for slot, cmds := range cmdsMap { - node, err := state.slotMasterNode(slot) - if err != nil { - setCmdsErr(cmds, err) - continue - } + node, err := state.slotMasterNode(slot) + if err != nil { + setCmdsErr(cmds, err) + return err + } - cmdsMap := map[*clusterNode][]Cmder{node: cmds} - for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { - if attempt > 0 { - if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { - setCmdsErr(cmds, err) - return err - } + cmdsMap := map[*clusterNode][]Cmder{node: cmds} + for attempt := 0; attempt <= c.opt.MaxRedirects; attempt++ { + if attempt > 0 { + if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil { + setCmdsErr(cmds, err) + return err } + } - failedCmds := newCmdsMap() - var wg sync.WaitGroup + failedCmds := newCmdsMap() + var wg sync.WaitGroup - for node, cmds := range cmdsMap { - wg.Add(1) - go func(node *clusterNode, cmds []Cmder) { - defer wg.Done() - c.processTxPipelineNode(ctx, node, cmds, failedCmds) - }(node, cmds) - } + for node, cmds := range cmdsMap { + wg.Add(1) + go func(node *clusterNode, cmds []Cmder) { + defer wg.Done() + c.processTxPipelineNode(ctx, node, cmds, failedCmds) + }(node, cmds) + } - wg.Wait() - if len(failedCmds.m) == 0 { - break - } - cmdsMap = failedCmds.m + wg.Wait() + if len(failedCmds.m) == 0 { + break } + cmdsMap = failedCmds.m } return cmdsFirstErr(cmds) } -func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder { - cmdsMap := make(map[int][]Cmder) +// slottedKeyedCommands returns a map of slot to commands taking into account +// only commands that have keys. +func (c *ClusterClient) slottedKeyedCommands(cmds []Cmder) map[int][]Cmder { + cmdsSlots := map[int][]Cmder{} + + preferredRandomSlot := -1 for _, cmd := range cmds { - slot := c.cmdSlot(cmd) - cmdsMap[slot] = append(cmdsMap[slot], cmd) + if cmdFirstKeyPos(cmd) == 0 { + continue + } + + slot := c.cmdSlot(cmd, preferredRandomSlot) + if preferredRandomSlot == -1 { + preferredRandomSlot = slot + } + + cmdsSlots[slot] = append(cmdsSlots[slot], cmd) } - return cmdsMap + + return cmdsSlots } func (c *ClusterClient) processTxPipelineNode( @@ -1885,17 +1912,20 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { return info } -func (c *ClusterClient) cmdSlot(cmd Cmder) int { +func (c *ClusterClient) cmdSlot(cmd Cmder, preferredRandomSlot int) int { args := cmd.Args() if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { return args[2].(int) } - return cmdSlot(cmd, cmdFirstKeyPos(cmd)) + return cmdSlot(cmd, cmdFirstKeyPos(cmd), preferredRandomSlot) } -func cmdSlot(cmd Cmder, pos int) int { +func cmdSlot(cmd Cmder, pos int, preferredRandomSlot int) int { if pos == 0 { + if preferredRandomSlot != -1 { + return preferredRandomSlot + } return hashtag.RandomSlot() } firstKey := cmd.stringArg(pos) diff --git a/osscluster_test.go b/osscluster_test.go index 10023218d1..2c7f40a5f8 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -603,6 +603,15 @@ var _ = Describe("ClusterClient", func() { Expect(err).To(MatchError(redis.ErrCrossSlot)) }) + It("works normally with keyless commands and no CrossSlot error", func() { + pipe.Set(ctx, "A{s}", "A_value", 0) + pipe.Ping(ctx) + pipe.Set(ctx, "B{s}", "B_value", 0) + pipe.Ping(ctx) + _, err := pipe.Exec(ctx) + Expect(err).To(Not(HaveOccurred())) + }) + // doesn't fail when no commands are queued It("returns no error when there are no commands", func() { _, err := pipe.Exec(ctx) diff --git a/ring_test.go b/ring_test.go index ef95e9805d..5fd7d98236 100644 --- a/ring_test.go +++ b/ring_test.go @@ -304,7 +304,7 @@ var _ = Describe("Redis Ring", func() { ring = redis.NewRing(opt) }) It("supports Process hook", func() { - err := ring.Ping(ctx).Err() + err := ring.Set(ctx, "key", "test", 0).Err() Expect(err).NotTo(HaveOccurred()) var stack []string @@ -312,12 +312,12 @@ var _ = Describe("Redis Ring", func() { ring.AddHook(&hook{ processHook: func(hook redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - Expect(cmd.String()).To(Equal("ping: ")) + Expect(cmd.String()).To(Equal("get key: ")) stack = append(stack, "ring.BeforeProcess") err := hook(ctx, cmd) - Expect(cmd.String()).To(Equal("ping: PONG")) + Expect(cmd.String()).To(Equal("get key: test")) stack = append(stack, "ring.AfterProcess") return err @@ -329,12 +329,12 @@ var _ = Describe("Redis Ring", func() { shard.AddHook(&hook{ processHook: func(hook redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - Expect(cmd.String()).To(Equal("ping: ")) + Expect(cmd.String()).To(Equal("get key: ")) stack = append(stack, "shard.BeforeProcess") err := hook(ctx, cmd) - Expect(cmd.String()).To(Equal("ping: PONG")) + Expect(cmd.String()).To(Equal("get key: test")) stack = append(stack, "shard.AfterProcess") return err @@ -344,7 +344,7 @@ var _ = Describe("Redis Ring", func() { return nil }) - err = ring.Ping(ctx).Err() + err = ring.Get(ctx, "key").Err() Expect(err).NotTo(HaveOccurred()) Expect(stack).To(Equal([]string{ "ring.BeforeProcess", From ad69df05f6518cfdede6ee6949d2c9d079ac8d34 Mon Sep 17 00:00:00 2001 From: Warnar Boekkooi Date: Tue, 24 Jun 2025 09:53:35 +0200 Subject: [PATCH 202/230] feat(redisotel): add WithCallerEnabled option (#3415) * feat(redisotel): add WithCaller option Allow the disabling the collection of the `code.function`, `code.filepath` and `code.lineno` tracing attributes. When setting `WithCaller(false)` overall performance is increased as the "expensive" `runtime.Callers` and `runtime.(*Frames).Next` calls are no longer needed. * chore(redisotel): improve docblock language * chore(redisotel): rename `WithCaller` to `WithCallerEnabled` --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- extra/redisotel/config.go | 11 ++++++++++- extra/redisotel/tracing.go | 28 +++++++++++++++++----------- extra/redisotel/tracing_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/extra/redisotel/config.go b/extra/redisotel/config.go index c02ee0b312..6ebd4bd56a 100644 --- a/extra/redisotel/config.go +++ b/extra/redisotel/config.go @@ -20,6 +20,7 @@ type config struct { tracer trace.Tracer dbStmtEnabled bool + callerEnabled bool // Metrics options. @@ -57,6 +58,7 @@ func newConfig(opts ...baseOption) *config { tp: otel.GetTracerProvider(), mp: otel.GetMeterProvider(), dbStmtEnabled: true, + callerEnabled: true, } for _, opt := range opts { @@ -106,13 +108,20 @@ func WithTracerProvider(provider trace.TracerProvider) TracingOption { }) } -// WithDBStatement tells the tracing hook not to log raw redis commands. +// WithDBStatement tells the tracing hook to log raw redis commands. func WithDBStatement(on bool) TracingOption { return tracingOption(func(conf *config) { conf.dbStmtEnabled = on }) } +// WithCallerEnabled tells the tracing hook to log the calling function, file and line. +func WithCallerEnabled(on bool) TracingOption { + return tracingOption(func(conf *config) { + conf.callerEnabled = on + }) +} + //------------------------------------------------------------------------------ type MetricsOption interface { diff --git a/extra/redisotel/tracing.go b/extra/redisotel/tracing.go index 33b7abac18..40df5a2025 100644 --- a/extra/redisotel/tracing.go +++ b/extra/redisotel/tracing.go @@ -101,14 +101,16 @@ func (th *tracingHook) DialHook(hook redis.DialHook) redis.DialHook { func (th *tracingHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { - fn, file, line := funcFileLine("github.com/redis/go-redis") attrs := make([]attribute.KeyValue, 0, 8) - attrs = append(attrs, - semconv.CodeFunction(fn), - semconv.CodeFilepath(file), - semconv.CodeLineNumber(line), - ) + if th.conf.callerEnabled { + fn, file, line := funcFileLine("github.com/redis/go-redis") + attrs = append(attrs, + semconv.CodeFunction(fn), + semconv.CodeFilepath(file), + semconv.CodeLineNumber(line), + ) + } if th.conf.dbStmtEnabled { cmdString := rediscmd.CmdString(cmd) @@ -133,16 +135,20 @@ func (th *tracingHook) ProcessPipelineHook( hook redis.ProcessPipelineHook, ) redis.ProcessPipelineHook { return func(ctx context.Context, cmds []redis.Cmder) error { - fn, file, line := funcFileLine("github.com/redis/go-redis") - attrs := make([]attribute.KeyValue, 0, 8) attrs = append(attrs, - semconv.CodeFunction(fn), - semconv.CodeFilepath(file), - semconv.CodeLineNumber(line), attribute.Int("db.redis.num_cmd", len(cmds)), ) + if th.conf.callerEnabled { + fn, file, line := funcFileLine("github.com/redis/go-redis") + attrs = append(attrs, + semconv.CodeFunction(fn), + semconv.CodeFilepath(file), + semconv.CodeLineNumber(line), + ) + } + summary, cmdsString := rediscmd.CmdsString(cmds) if th.conf.dbStmtEnabled { attrs = append(attrs, semconv.DBStatement(cmdsString)) diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go index e5ef86edcc..a3e3ccc62e 100644 --- a/extra/redisotel/tracing_test.go +++ b/extra/redisotel/tracing_test.go @@ -66,6 +66,35 @@ func TestWithDBStatement(t *testing.T) { } } +func TestWithoutCaller(t *testing.T) { + provider := sdktrace.NewTracerProvider() + hook := newTracingHook( + "", + WithTracerProvider(provider), + WithCallerEnabled(false), + ) + ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") + cmd := redis.NewCmd(ctx, "ping") + defer span.End() + + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() + for _, attr := range attrs { + switch attr.Key { + case semconv.CodeFunctionKey, + semconv.CodeFilepathKey, + semconv.CodeLineNumberKey: + t.Fatalf("Attribute with %s statement should not exist", attr.Key) + } + } + return nil + }) + err := processHook(ctx, cmd) + if err != nil { + t.Fatal(err) + } +} + func TestTracingHook_DialHook(t *testing.T) { imsb := tracetest.NewInMemoryExporter() provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) From 4ff1bbea1f35e1f2da33eb37642871fc5b1eb237 Mon Sep 17 00:00:00 2001 From: Damian Cherubini Date: Tue, 24 Jun 2025 07:28:54 -0300 Subject: [PATCH 203/230] feat(client): Add CredentialsProvider field to UniversalOptions (#2927) * Add CredentialsProvider field to universal client * fix(options): Add credentials providers to universal options and pass to client options * chore(ring): Add missing fields in building clientOptions --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- ring.go | 31 +++++++++++++++++++++++----- sentinel.go | 40 ++++++++++++++++++++++++++++-------- universal.go | 58 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/ring.go b/ring.go index 1f75913a3e..ba4f94eed6 100644 --- a/ring.go +++ b/ring.go @@ -13,6 +13,7 @@ import ( "github.com/cespare/xxhash/v2" "github.com/dgryski/go-rendezvous" //nolint + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" @@ -73,7 +74,24 @@ type RingOptions struct { Protocol int Username string Password string - DB int + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + DB int MaxRetries int MinRetryBackoff time.Duration @@ -154,10 +172,13 @@ func (opt *RingOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, - DB: opt.DB, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, + DB: opt.DB, MaxRetries: -1, diff --git a/sentinel.go b/sentinel.go index 43fbcd2443..04c0f72693 100644 --- a/sentinel.go +++ b/sentinel.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/rand" @@ -60,7 +61,24 @@ type FailoverOptions struct { Protocol int Username string Password string - DB int + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + DB int MaxRetries int MinRetryBackoff time.Duration @@ -107,10 +125,13 @@ func (opt *FailoverOptions) clientOptions() *Options { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - DB: opt.DB, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, + DB: opt.DB, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, MaxRetries: opt.MaxRetries, MinRetryBackoff: opt.MinRetryBackoff, @@ -187,9 +208,12 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { Dialer: opt.Dialer, OnConnect: opt.OnConnect, - Protocol: opt.Protocol, - Username: opt.Username, - Password: opt.Password, + Protocol: opt.Protocol, + Username: opt.Username, + Password: opt.Password, + CredentialsProvider: opt.CredentialsProvider, + CredentialsProviderContext: opt.CredentialsProviderContext, + StreamingCredentialsProvider: opt.StreamingCredentialsProvider, MaxRedirects: opt.MaxRetries, diff --git a/universal.go b/universal.go index a1ce17bac3..9d51b928e6 100644 --- a/universal.go +++ b/universal.go @@ -5,6 +5,8 @@ import ( "crypto/tls" "net" "time" + + "github.com/redis/go-redis/v9/auth" ) // UniversalOptions information is required by UniversalClient to establish @@ -26,9 +28,27 @@ type UniversalOptions struct { Dialer func(ctx context.Context, network, addr string) (net.Conn, error) OnConnect func(ctx context.Context, cn *Conn) error - Protocol int - Username string - Password string + Protocol int + Username string + Password string + // CredentialsProvider allows the username and password to be updated + // before reconnecting. It should return the current username and password. + CredentialsProvider func() (username string, password string) + + // CredentialsProviderContext is an enhanced parameter of CredentialsProvider, + // done to maintain API compatibility. In the future, + // there might be a merge between CredentialsProviderContext and CredentialsProvider. + // There will be a conflict between them; if CredentialsProviderContext exists, we will ignore CredentialsProvider. + CredentialsProviderContext func(ctx context.Context) (username string, password string, err error) + + // StreamingCredentialsProvider is used to retrieve the credentials + // for the connection from an external source. Those credentials may change + // during the connection lifetime. This is useful for managed identity + // scenarios where the credentials are retrieved from an external source. + // + // Currently, this is a placeholder for the future implementation. + StreamingCredentialsProvider auth.StreamingCredentialsProvider + SentinelUsername string SentinelPassword string @@ -96,9 +116,12 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { Dialer: o.Dialer, OnConnect: o.OnConnect, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, MaxRedirects: o.MaxRedirects, ReadOnly: o.ReadOnly, @@ -147,10 +170,14 @@ func (o *UniversalOptions) Failover() *FailoverOptions { Dialer: o.Dialer, OnConnect: o.OnConnect, - DB: o.DB, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + DB: o.DB, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, + SentinelUsername: o.SentinelUsername, SentinelPassword: o.SentinelPassword, @@ -199,10 +226,13 @@ func (o *UniversalOptions) Simple() *Options { Dialer: o.Dialer, OnConnect: o.OnConnect, - DB: o.DB, - Protocol: o.Protocol, - Username: o.Username, - Password: o.Password, + DB: o.DB, + Protocol: o.Protocol, + Username: o.Username, + Password: o.Password, + CredentialsProvider: o.CredentialsProvider, + CredentialsProviderContext: o.CredentialsProviderContext, + StreamingCredentialsProvider: o.StreamingCredentialsProvider, MaxRetries: o.MaxRetries, MinRetryBackoff: o.MinRetryBackoff, From 8bb52c35019d6b6c1e5461072aba02100ecd237a Mon Sep 17 00:00:00 2001 From: Pete Woods Date: Tue, 24 Jun 2025 11:43:03 +0100 Subject: [PATCH 204/230] Set correct cluster slot for scan commands, similarly to Java's Jedis client (#2623) - At present, the `scan` command is dispatched to a random slot. - As far as I can tell, the scanX family of commands are not cluster aware (e.g. don't redirect the client to the correct slot). - You can see [here](https://github.com/redis/jedis/blob/869dc0bb6625b85c8bf15bf1361bde485a304338/src/main/java/redis/clients/jedis/ShardedCommandObjects.java#L101), the Jedis client calling `processKey` on the match argument, and this is what this PR also does. We've had this patch running in production, and it seems to work well for us. For further thought: - Continuing looking at other Redis clients (e.g. Jedis), they outright [reject as invalid](https://github.com/redis/jedis/blob/869dc0bb6625b85c8bf15bf1361bde485a304338/src/main/java/redis/clients/jedis/ShardedCommandObjects.java#L98) any scan command that does not include a hash-tag. Presumably this has the advantage of users not being surprised when their scan produces no results when a random server is picked. - Perhaps it would be sensible for go-redis to do the same also? Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- generic_commands.go | 8 ++++++++ hash_commands.go | 8 ++++++++ internal/hashtag/hashtag.go | 12 ++++++++++++ internal/hashtag/hashtag_test.go | 25 +++++++++++++++++++++++++ set_commands.go | 9 ++++++++- sortedset_commands.go | 5 +++++ 6 files changed, 66 insertions(+), 1 deletion(-) diff --git a/generic_commands.go b/generic_commands.go index dc6c3fe014..c7100222cd 100644 --- a/generic_commands.go +++ b/generic_commands.go @@ -3,6 +3,8 @@ package redis import ( "context" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type GenericCmdable interface { @@ -363,6 +365,9 @@ func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count in args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(3) + } _ = c(ctx, cmd) return cmd } @@ -379,6 +384,9 @@ func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, coun args = append(args, "type", keyType) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(3) + } _ = c(ctx, cmd) return cmd } diff --git a/hash_commands.go b/hash_commands.go index 98a361b3ef..335cb950d9 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -3,6 +3,8 @@ package redis import ( "context" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type HashCmdable interface { @@ -192,6 +194,9 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } @@ -211,6 +216,9 @@ func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, m } args = append(args, "novalues") cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } diff --git a/internal/hashtag/hashtag.go b/internal/hashtag/hashtag.go index f13ee816d6..ea56fd6c7c 100644 --- a/internal/hashtag/hashtag.go +++ b/internal/hashtag/hashtag.go @@ -56,6 +56,18 @@ func Key(key string) string { return key } +func Present(key string) bool { + if key == "" { + return false + } + if s := strings.IndexByte(key, '{'); s > -1 { + if e := strings.IndexByte(key[s+1:], '}'); e > 0 { + return true + } + } + return false +} + func RandomSlot() int { return rand.Intn(slotNumber) } diff --git a/internal/hashtag/hashtag_test.go b/internal/hashtag/hashtag_test.go index fe4865b74f..983e3928c6 100644 --- a/internal/hashtag/hashtag_test.go +++ b/internal/hashtag/hashtag_test.go @@ -69,3 +69,28 @@ var _ = Describe("HashSlot", func() { } }) }) + +var _ = Describe("Present", func() { + It("should calculate hash slots", func() { + tests := []struct { + key string + present bool + }{ + {"123456789", false}, + {"{}foo", false}, + {"foo{}", false}, + {"foo{}{bar}", false}, + {"", false}, + {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), false}, + {"foo{bar}", true}, + {"{foo}bar", true}, + {"{user1000}.following", true}, + {"foo{{bar}}zap", true}, + {"foo{bar}{zap}", true}, + } + + for _, test := range tests { + Expect(Present(test.key)).To(Equal(test.present), "for %s", test.key) + } + }) +}) diff --git a/set_commands.go b/set_commands.go index 355f514a06..79efa6e405 100644 --- a/set_commands.go +++ b/set_commands.go @@ -1,6 +1,10 @@ package redis -import "context" +import ( + "context" + + "github.com/redis/go-redis/v9/internal/hashtag" +) type SetCmdable interface { SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd @@ -211,6 +215,9 @@ func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } diff --git a/sortedset_commands.go b/sortedset_commands.go index 14b3585882..e48e736714 100644 --- a/sortedset_commands.go +++ b/sortedset_commands.go @@ -4,6 +4,8 @@ import ( "context" "strings" "time" + + "github.com/redis/go-redis/v9/internal/hashtag" ) type SortedSetCmdable interface { @@ -719,6 +721,9 @@ func (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match str args = append(args, "count", count) } cmd := NewScanCmd(ctx, c, args...) + if hashtag.Present(match) { + cmd.SetFirstKeyPos(4) + } _ = c(ctx, cmd) return cmd } From a0e91bcaf177af773c67d4087aff041d33d410ef Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:05:24 +0300 Subject: [PATCH 205/230] chore(release): v9.11.0 (#3416) * chore(release): update version to 9.11.0 * chore(release): Add Release Notes for v9.11.0 --- RELEASE-NOTES.md | 38 +++++++++++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 ++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 +-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 +-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 13 files changed, 54 insertions(+), 16 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f6a4abb921..64754902da 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,43 @@ # Release Notes +# 9.11.0 (2025-06-24) + +## 🚀 Highlights + +Fixes TxPipeline to work correctly in cluster scenarios, allowing execution of commands +only in the same slot. + +# Changes + +## 🚀 New Features + +- Set cluster slot for `scan` commands, rather than random ([#2623](https://github.com/redis/go-redis/pull/2623)) +- Add CredentialsProvider field to UniversalOptions ([#2927](https://github.com/redis/go-redis/pull/2927)) +- feat(redisotel): add WithCallerEnabled option ([#3415](https://github.com/redis/go-redis/pull/3415)) + +## 🐛 Bug Fixes + +- fix(txpipeline): keyless commands should take the slot of the keyed ([#3411](https://github.com/redis/go-redis/pull/3411)) +- fix(loading): cache the loaded flag for slave nodes ([#3410](https://github.com/redis/go-redis/pull/3410)) +- fix(txpipeline): should return error on multi/exec on multiple slots ([#3408](https://github.com/redis/go-redis/pull/3408)) +- fix: check if the shard exists to avoid returning nil ([#3396](https://github.com/redis/go-redis/pull/3396)) + +## 🧰 Maintenance + +- feat: optimize connection pool waitTurn ([#3412](https://github.com/redis/go-redis/pull/3412)) +- chore(ci): update CI redis builds ([#3407](https://github.com/redis/go-redis/pull/3407)) +- chore: remove a redundant method from `Ring`, `Client` and `ClusterClient` ([#3401](https://github.com/redis/go-redis/pull/3401)) +- test: refactor TestBasicCredentials using table-driven tests ([#3406](https://github.com/redis/go-redis/pull/3406)) +- perf: reduce unnecessary memory allocation operations ([#3399](https://github.com/redis/go-redis/pull/3399)) +- fix: insert entry during iterating over a map ([#3398](https://github.com/redis/go-redis/pull/3398)) +- DOC-5229 probabilistic data type examples ([#3413](https://github.com/redis/go-redis/pull/3413)) +- chore(deps): bump rojopolis/spellcheck-github-actions from 0.49.0 to 0.51.0 ([#3414](https://github.com/redis/go-redis/pull/3414)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@andy-stark-redis](https://github.com/andy-stark-redis), [@boekkooi-impossiblecloud](https://github.com/boekkooi-impossiblecloud), [@cxljs](https://github.com/cxljs), [@dcherubini](https://github.com/dcherubini), [@dependabot[bot]](https://github.com/apps/dependabot), [@iamamirsalehi](https://github.com/iamamirsalehi), [@ndyakov](https://github.com/ndyakov), [@pete-woods](https://github.com/pete-woods), [@twz915](https://github.com/twz915) and [dependabot[bot]](https://github.com/apps/dependabot) + # 9.10.0 (2025-06-06) ## 🚀 Highlights diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index eac5651aea..08144430f3 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index b0769c4806..19611d46c5 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.10.0 +require github.com/redis/go-redis/v9 v9.11.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index ee6a219d8b..89293593d4 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index e501e03e93..1706c42e9a 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.10.0 +require github.com/redis/go-redis/v9 v9.11.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 97fc824e9f..26653fbc1e 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.10.0 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/extra/redisotel/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.11.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 48fdc4e44b..6eb04204ad 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.10.0 +require github.com/redis/go-redis/v9 v9.11.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index ee6a219d8b..89293593d4 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index c06d98084f..5e01aba612 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.11.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index b86582fc75..c8e8f3c2fb 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 1e415da6a8..b3c2db5fde 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.10.0 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.11.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index e1b40f96a1..74613deb36 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.10.0 + github.com/redis/go-redis/v9 v9.11.0 ) require ( diff --git a/version.go b/version.go index cbed8bd8d2..e6dbfd14e3 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.10.0" + return "9.11.0" } From c2e21269664d7e19fd6635505812d535af1b4e94 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 2 Jul 2025 17:19:24 +0300 Subject: [PATCH 206/230] feat(bitop): add support for the new bitop operations (#3409) * Add support for new bitop operations * chore(ci): Add 8.2 pre build for CI * feat(info): add new client info keys * fixed tests * added godocs for bitop commands --------- Co-authored-by: Nedyalko Dyakov Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- .github/actions/run-tests/action.yml | 1 + .github/workflows/build.yml | 3 ++ bitmap_commands.go | 32 ++++++++++++ command.go | 9 ++++ commands_test.go | 76 ++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index e9bb7e797b..bba991972b 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,6 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( + ["8.2.x"]="8.2-M01-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8b3d1b686..dd097a9fa6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: fail-fast: false matrix: redis-version: + - "8.2.x" # Redis CE 8.2 - "8.0.x" # Redis CE 8.0 - "7.4.x" # Redis stack 7.4 go-version: @@ -43,6 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( + ["8.2.x"]="8.2-M01-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) @@ -72,6 +74,7 @@ jobs: fail-fast: false matrix: redis-version: + - "8.2.x" # Redis CE 8.2 - "8.0.x" # Redis CE 8.0 - "7.4.x" # Redis stack 7.4 - "7.2.x" # Redis stack 7.2 diff --git a/bitmap_commands.go b/bitmap_commands.go index a215582890..4dbc862a19 100644 --- a/bitmap_commands.go +++ b/bitmap_commands.go @@ -12,6 +12,10 @@ type BitMapCmdable interface { BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd + BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd BitOpNot(ctx context.Context, destKey string, key string) *IntCmd BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd @@ -78,22 +82,50 @@ func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) return cmd } +// BitOpAnd creates a new bitmap in which users are members of all given bitmaps func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "and", destKey, keys...) } +// BitOpOr creates a new bitmap in which users are member of at least one given bitmap func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "or", destKey, keys...) } +// BitOpXor creates a new bitmap in which users are the result of XORing all given bitmaps func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd { return c.bitOp(ctx, "xor", destKey, keys...) } +// BitOpNot creates a new bitmap in which users are not members of a given bitmap func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd { return c.bitOp(ctx, "not", destKey, key) } +// BitOpDiff creates a new bitmap in which users are members of bitmap X but not of any of bitmaps Y1, Y2, … +// Introduced with Redis 8.2 +func (c cmdable) BitOpDiff(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "diff", destKey, keys...) +} + +// BitOpDiff1 creates a new bitmap in which users are members of one or more of bitmaps Y1, Y2, … but not members of bitmap X +// Introduced with Redis 8.2 +func (c cmdable) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "diff1", destKey, keys...) +} + +// BitOpAndOr creates a new bitmap in which users are members of bitmap X and also members of one or more of bitmaps Y1, Y2, … +// Introduced with Redis 8.2 +func (c cmdable) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "andor", destKey, keys...) +} + +// BitOpOne creates a new bitmap in which users are members of exactly one of the given bitmaps +// Introduced with Redis 8.2 +func (c cmdable) BitOpOne(ctx context.Context, destKey string, keys ...string) *IntCmd { + return c.bitOp(ctx, "one", destKey, keys...) +} + // BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end // if you need the `byte | bit` parameter, please use `BitPosSpan`. func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd { diff --git a/command.go b/command.go index b79338cb92..d3fb231b5e 100644 --- a/command.go +++ b/command.go @@ -5197,6 +5197,9 @@ type ClientInfo struct { OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full) OutputMemory int // omem, output buffer memory usage TotalMemory int // tot-mem, total memory consumed by this client in its various buffers + TotalNetIn int // tot-net-in, total network input + TotalNetOut int // tot-net-out, total network output + TotalCmds int // tot-cmds, total number of commands processed IoThread int // io-thread id Events string // file descriptor events (see below) LastCmd string // cmd, last command played @@ -5362,6 +5365,12 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.OutputMemory, err = strconv.Atoi(val) case "tot-mem": info.TotalMemory, err = strconv.Atoi(val) + case "tot-net-in": + info.TotalNetIn, err = strconv.Atoi(val) + case "tot-net-out": + info.TotalNetOut, err = strconv.Atoi(val) + case "tot-cmds": + info.TotalCmds, err = strconv.Atoi(val) case "events": info.Events = val case "cmd": diff --git a/commands_test.go b/commands_test.go index 72b206943b..19548e1347 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1469,6 +1469,82 @@ var _ = Describe("Commands", func() { Expect(get.Val()).To(Equal("\xf0")) }) + It("should BitOpDiff", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(8.2, "BITOP DIFF is available since Redis 8.2") + set := client.Set(ctx, "key1", "\xff", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + set = client.Set(ctx, "key2", "\x0f", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + bitOpDiff := client.BitOpDiff(ctx, "dest", "key1", "key2") + Expect(bitOpDiff.Err()).NotTo(HaveOccurred()) + Expect(bitOpDiff.Val()).To(Equal(int64(1))) + + get := client.Get(ctx, "dest") + Expect(get.Err()).NotTo(HaveOccurred()) + Expect(get.Val()).To(Equal("\xf0")) + }) + + It("should BitOpDiff1", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(8.2, "BITOP DIFF is available since Redis 8.2") + set := client.Set(ctx, "key1", "\xff", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + set = client.Set(ctx, "key2", "\x0f", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + bitOpDiff1 := client.BitOpDiff1(ctx, "dest", "key1", "key2") + Expect(bitOpDiff1.Err()).NotTo(HaveOccurred()) + Expect(bitOpDiff1.Val()).To(Equal(int64(1))) + + get := client.Get(ctx, "dest") + Expect(get.Err()).NotTo(HaveOccurred()) + Expect(get.Val()).To(Equal("\x00")) + }) + + It("should BitOpAndOr", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(8.2, "BITOP ANDOR is available since Redis 8.2") + set := client.Set(ctx, "key1", "\xff", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + set = client.Set(ctx, "key2", "\x0f", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + bitOpAndOr := client.BitOpAndOr(ctx, "dest", "key1", "key2") + Expect(bitOpAndOr.Err()).NotTo(HaveOccurred()) + Expect(bitOpAndOr.Val()).To(Equal(int64(1))) + + get := client.Get(ctx, "dest") + Expect(get.Err()).NotTo(HaveOccurred()) + Expect(get.Val()).To(Equal("\x0f")) + }) + + It("should BitOpOne", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(8.2, "BITOP ONE is available since Redis 8.2") + set := client.Set(ctx, "key1", "\xff", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + set = client.Set(ctx, "key2", "\x0f", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + bitOpOne := client.BitOpOne(ctx, "dest", "key1", "key2") + Expect(bitOpOne.Err()).NotTo(HaveOccurred()) + Expect(bitOpOne.Val()).To(Equal(int64(1))) + + get := client.Get(ctx, "dest") + Expect(get.Err()).NotTo(HaveOccurred()) + Expect(get.Val()).To(Equal("\xf0")) + }) + It("should BitOpNot", Label("NonRedisEnterprise"), func() { set := client.Set(ctx, "key1", "\x00", 0) Expect(set.Err()).NotTo(HaveOccurred()) From 705750e8da73e64339055490b859e49c320191f9 Mon Sep 17 00:00:00 2001 From: Nikita Semenov Date: Thu, 3 Jul 2025 14:48:06 +0700 Subject: [PATCH 207/230] fix: Ring.Pipelined return dial timeout error (#3403) * [ISSUE-3402]: Ring.Pipelined return dial timeout error * review fixes --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- ring.go | 16 +++++++++++++--- ring_test.go | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/ring.go b/ring.go index ba4f94eed6..0c15660197 100644 --- a/ring.go +++ b/ring.go @@ -798,6 +798,8 @@ func (c *Ring) generalProcessPipeline( } var wg sync.WaitGroup + errs := make(chan error, len(cmdsMap)) + for hash, cmds := range cmdsMap { wg.Add(1) go func(hash string, cmds []Cmder) { @@ -810,16 +812,24 @@ func (c *Ring) generalProcessPipeline( return } + hook := shard.Client.processPipelineHook if tx { cmds = wrapMultiExec(ctx, cmds) - _ = shard.Client.processTxPipelineHook(ctx, cmds) - } else { - _ = shard.Client.processPipelineHook(ctx, cmds) + hook = shard.Client.processTxPipelineHook + } + + if err = hook(ctx, cmds); err != nil { + errs <- err } }(hash, cmds) } wg.Wait() + close(errs) + + if err := <-errs; err != nil { + return err + } return cmdsFirstErr(cmds) } diff --git a/ring_test.go b/ring_test.go index 5fd7d98236..d35c0c5eab 100644 --- a/ring_test.go +++ b/ring_test.go @@ -277,6 +277,21 @@ var _ = Describe("Redis Ring", func() { Expect(ringShard1.Info(ctx).Val()).ToNot(ContainSubstring("keys=")) Expect(ringShard2.Info(ctx).Val()).To(ContainSubstring("keys=100")) }) + + It("return dial timeout error", func() { + opt := redisRingOptions() + opt.DialTimeout = 250 * time.Millisecond + opt.Addrs = map[string]string{"ringShardNotExist": ":1997"} + ring = redis.NewRing(opt) + + _, err := ring.Pipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.HSet(ctx, "key", "value") + pipe.Expire(ctx, "key", time.Minute) + return nil + }) + + Expect(err).To(HaveOccurred()) + }) }) Describe("new client callback", func() { From 7a03eb5bddb3a5f73d96171ecfbb1e95573a3ba8 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:32:04 +0100 Subject: [PATCH 208/230] DOC-4344 document quickstart examples (#3426) --- doctests/search_quickstart_test.go | 262 +++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 doctests/search_quickstart_test.go diff --git a/doctests/search_quickstart_test.go b/doctests/search_quickstart_test.go new file mode 100644 index 0000000000..ce5033bcfc --- /dev/null +++ b/doctests/search_quickstart_test.go @@ -0,0 +1,262 @@ +// EXAMPLE: search_quickstart +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +var bicycles = []interface{}{ + map[string]interface{}{ + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride " + + "for the smallest of tikes! This is the tiniest " + + "kids’ pedal bike on the market available without" + + " a coaster brake, the Jigger is the vehicle of " + + "choice for the rare tenacious little rider " + + "raring to go.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible." + + " Especially on an incline! They may be at the age " + + "when a 27.5\" wheel bike is just too clumsy coming " + + "off a 24\" bike. The Hillcraft 26 is just the solution" + + " they need!", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first" + + " experience on tracks and easy cruising through forests" + + " and fields. The lower top tube makes it easy to mount" + + " and dismount in any situation, giving your kids greater" + + " safety on the trails.", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the" + + " first and only women-dedicated bicycle brand. Designed" + + " by women for women, allEva bikes are optimized for the" + + " feminine physique using analytics from a body metrics" + + " database. If you like 29ers, try the Eva 291. It’s a " + + "brand new bike for 2022.. This full-suspension, " + + "cross-country ride has been designed for velocity. The" + + " 291 has 100mm of front and rear travel, a superlight " + + "aluminum frame and fast-rolling 29-inch wheels. Yippee!", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are " + + "looking for a lively trail bike that's just as inspiring" + + " on the climbs as it is over rougher ground, the Wilder" + + " is one heck of a bike built specifically for short women." + + " Both the frames and components have been tweaked to " + + "include a women’s saddle, different bars and unique " + + "colourway.", + "condition": "used", + }, + map[string]interface{}{ + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s" + + " not to say that it’s a basic machine. With an internal " + + "weld aluminium frame, a full carbon fork, and the slick-shifting" + + " Claris gears from Shimano’s, this is a bike which doesn’t" + + " break the bank and delivers craved performance.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still feel young" + + " at heart. It has a Bafang 1000W mid-drive system and a 48V" + + " 17.5AH Samsung Lithium-Ion battery, allowing you to ride for" + + " more than 60 miles on one charge. It’s great for tackling hilly" + + " terrain or if you just fancy a more leisurely ride. With three" + + " working modes, you can choose between E-bike, assisted bicycle," + + " and normal bike modes.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or back after" + + " a few minutes on the road, this lightweight, aluminum bike" + + " alleviates those issues and allows you to enjoy the ride. From" + + " the ergonomic grips to the lumbar-supporting seat position, the" + + " Roll Low-Entry offers incredible comfort. The rear-inclined seat" + + " tube facilitates stability by allowing you to put a foot on the" + + " ground to balance at a stop, and the low step-over frame makes it" + + " accessible for all ability and mobility levels. The saddle is" + + " very soft, with a wide back to support your hip joints and a" + + " cutout in the center to redistribute that pressure. Rim brakes" + + " deliver satisfactory braking control, and the wide tires provide" + + " a smooth, stable ride on paved roads and gravel. Rack and fender" + + " mounts facilitate setting up the Roll Low-Entry as your preferred" + + " commuter, and the BMX-like handlebar offers space for mounting a" + + " flashlight, bell, or phone holder.", + "condition": "new", + }, + map[string]interface{}{ + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both on bike" + + " paths and on the trail. The fork with 100mm of travel absorbs" + + " rough terrain. Fat Kenda Booster tires give you grip in corners" + + " and on wet trails. The Shimano Tourney drivetrain offered enough" + + " gears for finding a comfortable pace to ride uphill, and the" + + " Tektro hydraulic disc brakes break smoothly. Whether you want an" + + " affordable bike that you can take to work, but also take trail in" + + " mountains on the weekends or you’re just after a stable," + + " comfortable ride for the bike path, the Summit gives a good value" + + " for money.", + "condition": "new", + }, + map[string]interface{}{ + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as functional as it is" + + " pretty: The ThrillCycle steel frame offers a smooth ride. A" + + " 9-speed drivetrain has enough gears for coasting in the city, but" + + " we wouldn’t suggest taking it to the mountains. Fenders protect" + + " you from mud, and a rear basket lets you transport groceries," + + " flowers and books. The ThrillCycle comes with a limited lifetime" + + " warranty, so this little guy will last you long past graduation.", + "condition": "refurbished", + }, +} + +func ExampleClient_search_qs() { + // STEP_START connect + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // STEP_END + + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + // REMOVE_END + + // STEP_START create_index + schema := []*redis.FieldSchema{ + { + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + { + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + { + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + } + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + Prefix: []interface{}{"bicycle:"}, + OnJSON: true, + }, + schema..., + ).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START add_documents + for i, bicycle := range bicycles { + _, err := rdb.JSONSet( + ctx, + fmt.Sprintf("bicycle:%v", i), + "$", + bicycle, + ).Result() + + if err != nil { + panic(err) + } + } + // STEP_END + + // STEP_START wildcard_query + wCardResult, err := rdb.FTSearch(ctx, "idx:bicycle", "*").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Documents found: %v\n", wCardResult.Total) + // >>> Documents found: 10 + // STEP_END + + // STEP_START query_single_term + stResult, err := rdb.FTSearch( + ctx, + "idx:bicycle", + "@model:Jigger", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(stResult) + // >>> {1 [{bicycle:0 map[$:{"brand":"Velorim", ... + // STEP_END + + // STEP_START query_exact_matching + exactMatchResult, err := rdb.FTSearch( + ctx, + "idx:bicycle", + "@brand:\"Noka Bikes\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(exactMatchResult) + // >>> {1 [{bicycle:4 map[$:{"brand":"Noka Bikes"... + // STEP_END + + // Output: + // Documents found: 10 + // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}]}]} + // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}]}]} +} From 0d08e27a68467aafceedace9c1f35d35d1c607dd Mon Sep 17 00:00:00 2001 From: cxljs Date: Tue, 22 Jul 2025 17:45:41 +0800 Subject: [PATCH 209/230] fix: `errors.Join` requires Go 1.20 or later (#3442) Signed-off-by: Xiaolong Chen --- sentinel.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sentinel.go b/sentinel.go index 04c0f72693..4b4d450772 100644 --- a/sentinel.go +++ b/sentinel.go @@ -16,6 +16,7 @@ import ( "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/rand" + "github.com/redis/go-redis/v9/internal/util" ) //------------------------------------------------------------------------------ @@ -782,7 +783,20 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { for err := range errCh { errs = append(errs, err) } - return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %w", errors.Join(errs...)) + return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %s", joinErrors(errs)) +} + +func joinErrors(errs []error) string { + if len(errs) == 1 { + return errs[0].Error() + } + + b := []byte(errs[0].Error()) + for _, err := range errs[1:] { + b = append(b, '\n') + b = append(b, err.Error()...) + } + return util.BytesToString(b) } func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) { From f2d03dc581c8bdc937e9129870c6b918067cd881 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Tue, 22 Jul 2025 11:49:22 +0200 Subject: [PATCH 210/230] feat: Add "skip_verify" to Sentinel (#3428) Same as 3d4310ae but for FailoverOptions. Signed-off-by: Julien Riou --- sentinel.go | 5 +++++ sentinel_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/sentinel.go b/sentinel.go index 4b4d450772..83e4069d80 100644 --- a/sentinel.go +++ b/sentinel.go @@ -272,6 +272,7 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { // URL attributes (scheme, host, userinfo, resp.), query parameters using these // names will be treated as unknown parameters // - unknown parameter names will result in an error +// - use "skip_verify=true" to ignore TLS certificate validation // // Example: // @@ -379,6 +380,10 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, o.SentinelAddrs = append(o.SentinelAddrs, net.JoinHostPort(h, p)) } + if o.TLSConfig != nil && q.has("skip_verify") { + o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify") + } + // any parameters left? if r := q.remaining(); len(r) > 0 { return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", ")) diff --git a/sentinel_test.go b/sentinel_test.go index 436895ff2f..bfeb28161f 100644 --- a/sentinel_test.go +++ b/sentinel_test.go @@ -431,6 +431,14 @@ func TestParseFailoverURL(t *testing.T) { ServerName: "localhost", }}, }, + { + url: "rediss://localhost:6379/5?master_name=test&skip_verify=true", + o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test", DB: 5, + TLSConfig: &tls.Config{ + ServerName: "localhost", + InsecureSkipVerify: true, + }}, + }, { url: "redis://localhost:6379/5?master_name=test&db=2", o: &redis.FailoverOptions{SentinelAddrs: []string{"localhost:6379"}, MasterName: "test", DB: 2}, From 5c8d669bfdd20745b149a14e1131bb0c7f7b9ab9 Mon Sep 17 00:00:00 2001 From: Antonio Mindov Date: Thu, 24 Jul 2025 12:48:34 +0300 Subject: [PATCH 211/230] feat(otel): add closing support to otel metrics instrumentation (#3444) closes #3424 --- extra/redisotel/config.go | 8 +++ extra/redisotel/metrics.go | 117 +++++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/extra/redisotel/config.go b/extra/redisotel/config.go index 6ebd4bd56a..6d90abfd0d 100644 --- a/extra/redisotel/config.go +++ b/extra/redisotel/config.go @@ -28,6 +28,8 @@ type config struct { meter metric.Meter poolName string + + closeChan chan struct{} } type baseOption interface { @@ -145,3 +147,9 @@ func WithMeterProvider(mp metric.MeterProvider) MetricsOption { conf.mp = mp }) } + +func WithCloseChan(closeChan chan struct{}) MetricsOption { + return metricsOption(func(conf *config) { + conf.closeChan = closeChan + }) +} diff --git a/extra/redisotel/metrics.go b/extra/redisotel/metrics.go index 4974f4e8de..d9a1c72196 100644 --- a/extra/redisotel/metrics.go +++ b/extra/redisotel/metrics.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "sync" "time" "go.opentelemetry.io/otel" @@ -13,6 +14,12 @@ import ( "github.com/redis/go-redis/v9" ) +type metricsState struct { + registrations []metric.Registration + closed bool + mutex sync.Mutex +} + // InstrumentMetrics starts reporting OpenTelemetry Metrics. // // Based on https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/database-metrics.md @@ -30,49 +37,42 @@ func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error { ) } - switch rdb := rdb.(type) { - case *redis.Client: - if conf.poolName == "" { - opt := rdb.Options() - conf.poolName = opt.Addr + var state *metricsState + if conf.closeChan != nil { + state = &metricsState{ + registrations: make([]metric.Registration, 0), + closed: false, + mutex: sync.Mutex{}, } - conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) - if err := reportPoolStats(rdb, conf); err != nil { - return err - } - if err := addMetricsHook(rdb, conf); err != nil { - return err - } - return nil - case *redis.ClusterClient: - rdb.OnNewNode(func(rdb *redis.Client) { - if conf.poolName == "" { - opt := rdb.Options() - conf.poolName = opt.Addr - } - conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) + go func() { + <-conf.closeChan - if err := reportPoolStats(rdb, conf); err != nil { - otel.Handle(err) + state.mutex.Lock() + state.closed = true + + for _, registration := range state.registrations { + if err := registration.Unregister(); err != nil { + otel.Handle(err) + } } - if err := addMetricsHook(rdb, conf); err != nil { + state.mutex.Unlock() + }() + } + + switch rdb := rdb.(type) { + case *redis.Client: + return registerClient(rdb, conf, state) + case *redis.ClusterClient: + rdb.OnNewNode(func(rdb *redis.Client) { + if err := registerClient(rdb, conf, state); err != nil { otel.Handle(err) } }) return nil case *redis.Ring: rdb.OnNewNode(func(rdb *redis.Client) { - if conf.poolName == "" { - opt := rdb.Options() - conf.poolName = opt.Addr - } - conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) - - if err := reportPoolStats(rdb, conf); err != nil { - otel.Handle(err) - } - if err := addMetricsHook(rdb, conf); err != nil { + if err := registerClient(rdb, conf, state); err != nil { otel.Handle(err) } }) @@ -82,7 +82,38 @@ func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error { } } -func reportPoolStats(rdb *redis.Client, conf *config) error { +func registerClient(rdb *redis.Client, conf *config, state *metricsState) error { + if state != nil { + state.mutex.Lock() + defer state.mutex.Unlock() + + if state.closed { + return nil + } + } + + if conf.poolName == "" { + opt := rdb.Options() + conf.poolName = opt.Addr + } + conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) + + registration, err := reportPoolStats(rdb, conf) + if err != nil { + return err + } + + if state != nil { + state.registrations = append(state.registrations, registration) + } + + if err := addMetricsHook(rdb, conf); err != nil { + return err + } + return nil +} + +func reportPoolStats(rdb *redis.Client, conf *config) (metric.Registration, error) { labels := conf.attrs idleAttrs := append(labels, attribute.String("state", "idle")) usedAttrs := append(labels, attribute.String("state", "used")) @@ -92,7 +123,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The maximum number of idle open connections allowed"), ) if err != nil { - return err + return nil, err } idleMin, err := conf.meter.Int64ObservableUpDownCounter( @@ -100,7 +131,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The minimum number of idle open connections allowed"), ) if err != nil { - return err + return nil, err } connsMax, err := conf.meter.Int64ObservableUpDownCounter( @@ -108,7 +139,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The maximum number of open connections allowed"), ) if err != nil { - return err + return nil, err } usage, err := conf.meter.Int64ObservableUpDownCounter( @@ -116,7 +147,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The number of connections that are currently in state described by the state attribute"), ) if err != nil { - return err + return nil, err } timeouts, err := conf.meter.Int64ObservableUpDownCounter( @@ -124,7 +155,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The number of connection timeouts that have occurred trying to obtain a connection from the pool"), ) if err != nil { - return err + return nil, err } hits, err := conf.meter.Int64ObservableUpDownCounter( @@ -132,7 +163,7 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The number of times free connection was found in the pool"), ) if err != nil { - return err + return nil, err } misses, err := conf.meter.Int64ObservableUpDownCounter( @@ -140,11 +171,11 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { metric.WithDescription("The number of times free connection was not found in the pool"), ) if err != nil { - return err + return nil, err } redisConf := rdb.Options() - _, err = conf.meter.RegisterCallback( + return conf.meter.RegisterCallback( func(ctx context.Context, o metric.Observer) error { stats := rdb.PoolStats() @@ -168,8 +199,6 @@ func reportPoolStats(rdb *redis.Client, conf *config) error { hits, misses, ) - - return err } func addMetricsHook(rdb *redis.Client, conf *config) error { From 2067991a475edfb636e6e1fd073f0b0acaff9219 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 30 Jul 2025 13:42:18 +0300 Subject: [PATCH 212/230] chore(ci): bumped redis 8.2 version used in the CI/CD (#3451) --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index bba991972b..f826eccead 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-M01-pre" + ["8.2.x"]="8.2-RC1-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd097a9fa6..ef6eadfc66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-M01-pre" + ["8.2.x"]="8.2-RC1-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) From e6ea0056fed2a6d313e13779b41eac1bc274142b Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Thu, 31 Jul 2025 11:11:15 +0300 Subject: [PATCH 213/230] Added new stream commands (#3450) * added new stream commands * updated docker image * fixed command return type * fixed tests * addressed PR comments --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- commands_test.go | 59 ++++++++++++++++++++++++++++++++++++++ stream_commands.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/commands_test.go b/commands_test.go index 19548e1347..9e1300893a 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6169,6 +6169,34 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XTrimMaxLenMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenMode(ctx, "stream", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMaxLenApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenApproxMode(ctx, "stream", 0, 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDMode(ctx, "stream", "4-0", "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDApproxMode(ctx, "stream", "4-0", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + It("should XAdd", func() { id, err := client.XAdd(ctx, &redis.XAddArgs{ Stream: "stream", @@ -6222,6 +6250,37 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XAckDel", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // First, create a consumer group + err := client.XGroupCreate(ctx, "stream", "testgroup", "0").Err() + Expect(err).NotTo(HaveOccurred()) + + // Read messages to create pending entries + _, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{ + Group: "testgroup", + Consumer: "testconsumer", + Streams: []string{"stream", ">"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + + // Test XAckDel with KEEPREF mode + n, err := client.XAckDel(ctx, "stream", "testgroup", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + + // Clean up + client.XGroupDestroy(ctx, "stream", "testgroup") + }) + + It("should XDelEx", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // Test XDelEx with KEEPREF mode + n, err := client.XDelEx(ctx, "stream", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + }) + It("should XLen", func() { n, err := client.XLen(ctx, "stream").Result() Expect(err).NotTo(HaveOccurred()) diff --git a/stream_commands.go b/stream_commands.go index 6d7b229224..4b84e00fdd 100644 --- a/stream_commands.go +++ b/stream_commands.go @@ -7,7 +7,9 @@ import ( type StreamCmdable interface { XAdd(ctx context.Context, a *XAddArgs) *StringCmd + XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd XDel(ctx context.Context, stream string, ids ...string) *IntCmd + XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd XLen(ctx context.Context, stream string) *IntCmd XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd @@ -31,8 +33,12 @@ type StreamCmdable interface { XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd + XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd + XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd XTrimMinID(ctx context.Context, key string, minID string) *IntCmd XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd + XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd + XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd XInfoStream(ctx context.Context, key string) *XInfoStreamCmd XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd @@ -54,6 +60,7 @@ type XAddArgs struct { // Approx causes MaxLen and MinID to use "~" matcher (instead of "="). Approx bool Limit int64 + Mode string ID string Values interface{} } @@ -81,6 +88,11 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { if a.Limit > 0 { args = append(args, "limit", a.Limit) } + + if a.Mode != "" { + args = append(args, a.Mode) + } + if a.ID != "" { args = append(args, a.ID) } else { @@ -93,6 +105,16 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { return cmd } +func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd { args := []interface{}{"xdel", stream} for _, id := range ids { @@ -103,6 +125,16 @@ func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd return cmd } +func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xdelex", stream, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd { cmd := NewIntCmd(ctx, "xlen", stream) _ = c(ctx, cmd) @@ -375,6 +407,8 @@ func xClaimArgs(a *XClaimArgs) []interface{} { return args } +// TODO: refactor xTrim, xTrimMode and the wrappers over the functions + // xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default). // example: // @@ -418,6 +452,42 @@ func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, return c.xTrim(ctx, key, "minid", true, minID, limit) } +func (c cmdable) xTrimMode( + ctx context.Context, key, strategy string, + approx bool, threshold interface{}, limit int64, + mode string, +) *IntCmd { + args := make([]interface{}, 0, 7) + args = append(args, "xtrim", key, strategy) + if approx { + args = append(args, "~") + } + args = append(args, threshold) + if limit > 0 { + args = append(args, "limit", limit) + } + args = append(args, mode) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode) +} + +func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode) +} + +func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode) +} + +func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode) +} + func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd { cmd := NewXInfoConsumersCmd(ctx, key, group) _ = c(ctx, cmd) From 74a798736fe3639a80c8cf2270a36e3b9d76e357 Mon Sep 17 00:00:00 2001 From: cxljs Date: Thu, 31 Jul 2025 16:45:39 +0800 Subject: [PATCH 214/230] VSIM add `EPSILON` option (#3454) Signed-off-by: Xiaolong Chen --- vectorset_commands.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vectorset_commands.go b/vectorset_commands.go index 2bd9e22166..96be1af110 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -287,8 +287,7 @@ type VSimArgs struct { FilterEF int64 Truth bool NoThread bool - // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. - // Epsilon float64 + Epsilon float64 } func (v VSimArgs) appendArgs(args []any) []any { @@ -310,13 +309,13 @@ func (v VSimArgs) appendArgs(args []any) []any { if v.NoThread { args = append(args, "nothread") } - // if v.Epsilon > 0 { - // args = append(args, "Epsilon", v.Epsilon) - // } + if v.Epsilon > 0 { + args = append(args, "Epsilon", v.Epsilon) + } return args } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { @@ -331,7 +330,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr return cmd } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd { From 193d5a0b035423749729a05c662ee628e87cf921 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:21:05 +0300 Subject: [PATCH 215/230] fix(search): return results even if doc is empty (#3457) * fix(search): return results even if doc is empty "If a relevant key expires while a query is running, an attempt to load the key's value will return a null array. However, the key is still counted in the total number of results." - Redis Search return documentation * fix(doctest): fix assertions in doctests --- doctests/geo_index_test.go | 4 ++-- doctests/home_json_example_test.go | 4 ++-- doctests/search_quickstart_test.go | 4 ++-- search_commands.go | 9 ++++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go index c497b7224e..2e94427436 100644 --- a/doctests/geo_index_test.go +++ b/doctests/geo_index_test.go @@ -199,11 +199,11 @@ func ExampleClient_geoindex() { // OK // OK // OK - // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] }]} // OK // OK // OK // OK // OK - // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] }]} } diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index f32bf8d10f..d2af4d7389 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -219,7 +219,7 @@ func ExampleClient_search_json() { // STEP_END // Output: - // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} + // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] }]} // London // Tel Aviv // 0 @@ -329,5 +329,5 @@ func ExampleClient_search_hash() { // STEP_END // Output: - // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir]}]} + // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] }]} } diff --git a/doctests/search_quickstart_test.go b/doctests/search_quickstart_test.go index ce5033bcfc..a41c9c5cf5 100644 --- a/doctests/search_quickstart_test.go +++ b/doctests/search_quickstart_test.go @@ -257,6 +257,6 @@ func ExampleClient_search_qs() { // Output: // Documents found: 10 - // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}]}]} - // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}]}]} + // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] }]} + // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] }]} } diff --git a/search_commands.go b/search_commands.go index b31baaa760..6552f553e4 100644 --- a/search_commands.go +++ b/search_commands.go @@ -474,6 +474,7 @@ type Document struct { Payload *string SortKey *string Fields map[string]string + Error error } type AggregateQuery []interface{} @@ -1654,7 +1655,13 @@ func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, with if i < len(data) { fields, ok := data[i].([]interface{}) if !ok { - return FTSearchResult{}, fmt.Errorf("invalid document fields format") + if data[i] == proto.Nil || data[i] == nil { + doc.Error = proto.Nil + doc.Fields = map[string]string{} + fields = []interface{}{} + } else { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } } for j := 0; j < len(fields); j += 2 { From 3029f7c051ff82782767e607f03218e98fe3fb7c Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:19:40 +0300 Subject: [PATCH 216/230] chore(ci): Add 8.2 rc2 pre build for CI (#3459) * chore(ci): Add 8.2 rc2 pre build for CI * Updated gh username --------- Co-authored-by: Hristo Temelski --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index f826eccead..75b1282730 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-RC1-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef6eadfc66..8424f63c68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] pull_request: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] permissions: contents: read @@ -44,7 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-RC1-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) From cb484509d69c5e3de3da7a1a5a62a156b3069bd4 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:41:40 +0300 Subject: [PATCH 217/230] feat(search): Add VAMANA vector type to RediSearch (#3449) * Add VAMANA vector type to redisearch * Change to svs-vamana vector type && remove panics from search module * fix tests * fix tests * fix tests --- search_commands.go | 127 +++++++-- search_test.go | 654 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 753 insertions(+), 28 deletions(-) diff --git a/search_commands.go b/search_commands.go index 6552f553e4..f0ca1bfede 100644 --- a/search_commands.go +++ b/search_commands.go @@ -80,8 +80,9 @@ type FieldSchema struct { } type FTVectorArgs struct { - FlatOptions *FTFlatOptions - HNSWOptions *FTHNSWOptions + FlatOptions *FTFlatOptions + HNSWOptions *FTHNSWOptions + VamanaOptions *FTVamanaOptions } type FTFlatOptions struct { @@ -103,6 +104,19 @@ type FTHNSWOptions struct { Epsilon float64 } +type FTVamanaOptions struct { + Type string + Dim int + DistanceMetric string + Compression string + ConstructionWindowSize int + GraphMaxDegree int + SearchWindowSize int + Epsilon float64 + TrainingThreshold int + ReduceDim int +} + type FTDropIndexOptions struct { DeleteDocs bool } @@ -499,7 +513,7 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M return cmd } -func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery { +func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) { queryArgs := []interface{}{query} if options != nil { if options.Verbatim { @@ -515,7 +529,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + return nil, fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } if options.LoadAll { queryArgs = append(queryArgs, "LOAD", "*") @@ -571,7 +585,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery for _, sortBy := range options.SortBy { sortByOptions = append(sortByOptions, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") } if sortBy.Asc { sortByOptions = append(sortByOptions, "ASC") @@ -616,7 +630,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) { @@ -718,7 +732,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st args = append(args, "ADDSCORES") } if options.LoadAll && options.Load != nil { - panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")) + return cmd } if options.LoadAll { args = append(args, "LOAD", "*") @@ -771,7 +787,9 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st for _, sortBy := range options.SortBy { sortByOptions = append(sortByOptions, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.AGGREGATE: ASC and DESC are mutually exclusive") + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")) + return cmd } if sortBy.Asc { sortByOptions = append(sortByOptions, "ASC") @@ -919,7 +937,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, "ON", "JSON") } if options.OnHash && options.OnJSON { - panic("FT.CREATE: ON HASH and ON JSON are mutually exclusive") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: ON HASH and ON JSON are mutually exclusive")) + return cmd } if options.Prefix != nil { args = append(args, "PREFIX", len(options.Prefix)) @@ -970,12 +990,16 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp } } if schema == nil { - panic("FT.CREATE: SCHEMA is required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA is required")) + return cmd } args = append(args, "SCHEMA") for _, schema := range schema { if schema.FieldName == "" || schema.FieldType == SearchFieldTypeInvalid { - panic("FT.CREATE: SCHEMA FieldName and FieldType are required") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldName and FieldType are required")) + return cmd } args = append(args, schema.FieldName) if schema.As != "" { @@ -984,15 +1008,32 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, schema.FieldType.String()) if schema.VectorArgs != nil { if schema.FieldType != SearchFieldTypeVector { - panic("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType VECTOR is required for VectorArgs")) + return cmd + } + // Check mutual exclusivity of vector options + optionCount := 0 + if schema.VectorArgs.FlatOptions != nil { + optionCount++ + } + if schema.VectorArgs.HNSWOptions != nil { + optionCount++ + } + if schema.VectorArgs.VamanaOptions != nil { + optionCount++ } - if schema.VectorArgs.FlatOptions != nil && schema.VectorArgs.HNSWOptions != nil { - panic("FT.CREATE: SCHEMA VectorArgs FlatOptions and HNSWOptions are mutually exclusive") + if optionCount != 1 { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + return cmd } if schema.VectorArgs.FlatOptions != nil { args = append(args, "FLAT") if schema.VectorArgs.FlatOptions.Type == "" || schema.VectorArgs.FlatOptions.Dim == 0 || schema.VectorArgs.FlatOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR FLAT")) + return cmd } flatArgs := []interface{}{ "TYPE", schema.VectorArgs.FlatOptions.Type, @@ -1011,7 +1052,9 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.VectorArgs.HNSWOptions != nil { args = append(args, "HNSW") if schema.VectorArgs.HNSWOptions.Type == "" || schema.VectorArgs.HNSWOptions.Dim == 0 || schema.VectorArgs.HNSWOptions.DistanceMetric == "" { - panic("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR HNSW")) + return cmd } hnswArgs := []interface{}{ "TYPE", schema.VectorArgs.HNSWOptions.Type, @@ -1036,10 +1079,48 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, len(hnswArgs)) args = append(args, hnswArgs...) } + if schema.VectorArgs.VamanaOptions != nil { + args = append(args, "SVS-VAMANA") + if schema.VectorArgs.VamanaOptions.Type == "" || schema.VectorArgs.VamanaOptions.Dim == 0 || schema.VectorArgs.VamanaOptions.DistanceMetric == "" { + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + return cmd + } + vamanaArgs := []interface{}{ + "TYPE", schema.VectorArgs.VamanaOptions.Type, + "DIM", schema.VectorArgs.VamanaOptions.Dim, + "DISTANCE_METRIC", schema.VectorArgs.VamanaOptions.DistanceMetric, + } + if schema.VectorArgs.VamanaOptions.Compression != "" { + vamanaArgs = append(vamanaArgs, "COMPRESSION", schema.VectorArgs.VamanaOptions.Compression) + } + if schema.VectorArgs.VamanaOptions.ConstructionWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "CONSTRUCTION_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.ConstructionWindowSize) + } + if schema.VectorArgs.VamanaOptions.GraphMaxDegree > 0 { + vamanaArgs = append(vamanaArgs, "GRAPH_MAX_DEGREE", schema.VectorArgs.VamanaOptions.GraphMaxDegree) + } + if schema.VectorArgs.VamanaOptions.SearchWindowSize > 0 { + vamanaArgs = append(vamanaArgs, "SEARCH_WINDOW_SIZE", schema.VectorArgs.VamanaOptions.SearchWindowSize) + } + if schema.VectorArgs.VamanaOptions.Epsilon > 0 { + vamanaArgs = append(vamanaArgs, "EPSILON", schema.VectorArgs.VamanaOptions.Epsilon) + } + if schema.VectorArgs.VamanaOptions.TrainingThreshold > 0 { + vamanaArgs = append(vamanaArgs, "TRAINING_THRESHOLD", schema.VectorArgs.VamanaOptions.TrainingThreshold) + } + if schema.VectorArgs.VamanaOptions.ReduceDim > 0 { + vamanaArgs = append(vamanaArgs, "REDUCE", schema.VectorArgs.VamanaOptions.ReduceDim) + } + args = append(args, len(vamanaArgs)) + args = append(args, vamanaArgs...) + } } if schema.GeoShapeFieldType != "" { if schema.FieldType != SearchFieldTypeGeoShape { - panic("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType") + cmd := NewStatusCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.CREATE: SCHEMA FieldType GEOSHAPE is required for GeoShapeFieldType")) + return cmd } args = append(args, schema.GeoShapeFieldType) } @@ -1197,7 +1278,7 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri // FTExplainCli - Returns the execution plan for a complex query. [Not Implemented] // For more information, see https://redis.io/commands/ft.explaincli/ func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { - panic("not implemented") + return fmt.Errorf("FTExplainCli is not implemented") } func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { @@ -1758,7 +1839,7 @@ type SearchQuery []interface{} // For more information, please refer to the Redis documentation about [FT.SEARCH]. // // [FT.SEARCH]: (https://redis.io/commands/ft.search/) -func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { +func FTSearchQuery(query string, options *FTSearchOptions) (SearchQuery, error) { queryArgs := []interface{}{query} if options != nil { if options.NoContent { @@ -1838,7 +1919,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { for _, sortBy := range options.SortBy { queryArgs = append(queryArgs, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + return nil, fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive") } if sortBy.Asc { queryArgs = append(queryArgs, "ASC") @@ -1866,7 +1947,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { queryArgs = append(queryArgs, "DIALECT", 2) } } - return queryArgs + return queryArgs, nil } // FTSearchWithArgs - Executes a search query on an index with additional options. @@ -1955,7 +2036,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin for _, sortBy := range options.SortBy { args = append(args, sortBy.FieldName) if sortBy.Asc && sortBy.Desc { - panic("FT.SEARCH: ASC and DESC are mutually exclusive") + cmd := newFTSearchCmd(ctx, options, args...) + cmd.SetErr(fmt.Errorf("FT.SEARCH: ASC and DESC are mutually exclusive")) + return cmd } if sortBy.Asc { args = append(args, "ASC") diff --git a/search_test.go b/search_test.go index fdcd0d24b7..ede29c4613 100644 --- a/search_test.go +++ b/search_test.go @@ -38,6 +38,17 @@ func encodeFloat32Vector(vec []float32) []byte { return buf.Bytes() } +func encodeFloat16Vector(vec []float32) []byte { + buf := new(bytes.Buffer) + for _, v := range vec { + // Convert float32 to float16 (16-bit representation) + // This is a simplified conversion - in practice you'd use a proper float16 library + f16 := uint16(v * 1000) // Simple scaling for test purposes + binary.Write(buf, binary.LittleEndian, f16) + } + return buf.Bytes() +} + var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { ctx := context.TODO() var client *redis.Client @@ -819,7 +830,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should return only the base query when options is nil", Label("search", "ftaggregate"), func() { - args := redis.FTAggregateQuery("testQuery", nil) + args, err := redis.FTAggregateQuery("testQuery", nil) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(Equal(redis.AggregateQuery{"testQuery"})) }) @@ -828,7 +840,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Verbatim: true, Scorer: "BM25", } - args := redis.FTAggregateQuery("testQuery", options) + args, err := redis.FTAggregateQuery("testQuery", options) + Expect(err).NotTo(HaveOccurred()) Expect(args[0]).To(Equal("testQuery")) Expect(args).To(ContainElement("VERBATIM")) Expect(args).To(ContainElement("SCORER")) @@ -839,7 +852,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ AddScores: true, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("ADDSCORES")) }) @@ -847,7 +861,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ LoadAll: true, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("LOAD")) Expect(args).To(ContainElement("*")) }) @@ -859,7 +874,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { {Field: "field2"}, }, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) // Verify LOAD options related arguments Expect(args).To(ContainElement("LOAD")) // Check that field names and aliases are present @@ -872,7 +888,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { options := &redis.FTAggregateOptions{ Timeout: 500, } - args := redis.FTAggregateQuery("q", options) + args, err := redis.FTAggregateQuery("q", options) + Expect(err).NotTo(HaveOccurred()) Expect(args).To(ContainElement("TIMEOUT")) found := false for i, a := range args { @@ -1745,6 +1762,631 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(nanCount).To(Equal(2)) }) + It("should FTCreate VECTOR with VAMANA algorithm - basic", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + + It("should FTCreate VECTOR with VAMANA algorithm - with compression", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 256, + DistanceMetric: "COSINE", + Compression: "LVQ8", + TrainingThreshold: 10240, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + }) + + It("should FTCreate VECTOR with VAMANA algorithm - advanced parameters", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 512, + DistanceMetric: "IP", + Compression: "LVQ8", + ConstructionWindowSize: 300, + GraphMaxDegree: 128, + SearchWindowSize: 20, + Epsilon: 0.02, + TrainingThreshold: 20480, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + }) + + It("should fail FTCreate VECTOR with VAMANA - missing required parameters", Label("search", "ftcreate"), func() { + // Test missing Type + cmd := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Dim: 2, + DistanceMetric: "L2", + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + + // Test missing Dim + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + DistanceMetric: "L2", + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + + // Test missing DistanceMetric + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + }}}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("Type, Dim and DistanceMetric are required for VECTOR VAMANA")) + }) + + It("should fail FTCreate VECTOR with multiple vector options", Label("search", "ftcreate"), func() { + // Test VAMANA + HNSW + cmd := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{ + VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + HNSWOptions: &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + }}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + + // Test VAMANA + FLAT + cmd = client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{ + VamanaOptions: &redis.FTVamanaOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + FlatOptions: &redis.FTFlatOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"}, + }}) + Expect(cmd.Err()).To(HaveOccurred()) + Expect(cmd.Err().Error()).To(ContainSubstring("VectorArgs must have exactly one of FlatOptions, HNSWOptions, or VamanaOptions")) + }) + + It("should test VAMANA L2 distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // L2 distance test vectors + vectors := [][]float32{ + {1.0, 0.0, 0.0}, + {2.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, + {5.0, 0.0, 0.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA COSINE distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "COSINE", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 0.0, 0.0}, + {0.707, 0.707, 0.0}, + {0.0, 1.0, 0.0}, + {-1.0, 0.0, 0.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA IP distance metric", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 3, + DistanceMetric: "IP", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0}, + {2.0, 1.0, 1.0}, + {3.0, 3.0, 3.0}, + {0.1, 0.1, 0.1}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "score", Asc: true}}, + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc2")) + }) + + It("should test VAMANA basic functionality", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0, 4.0}, + {2.0, 3.0, 4.0, 5.0}, + {3.0, 4.0, 5.0, 6.0}, + {10.0, 11.0, 12.0, 13.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 3 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) // Should be closest to itself + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + + It("should test VAMANA FLOAT16 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.5, 2.5, 3.5, 4.5}, + {2.5, 3.5, 4.5, 5.5}, + {3.5, 4.5, 5.5, 6.5}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat16Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA FLOAT32 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 4, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := [][]float32{ + {1.0, 2.0, 3.0, 4.0}, + {2.0, 3.0, 4.0, 5.0}, + {3.0, 4.0, 5.0, 6.0}, + } + + for i, vec := range vectors { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA with default dialect", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 2, + DistanceMetric: "L2", + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(2)) + }) + + It("should test VAMANA with LVQ8 compression", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA compression with both vector types", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + // Test FLOAT16 with LVQ8 + vamanaOptions16 := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx16", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v16", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions16}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx16") + + // Test FLOAT32 with LVQ8 + vamanaOptions32 := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + } + val, err = client.FTCreate(ctx, "idx32", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v32", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions32}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx32") + + // Add data to both indices + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc16_%d", i), "v16", encodeFloat16Vector(vec)) + client.HSet(ctx, fmt.Sprintf("doc32_%d", i), "v32", encodeFloat32Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + + // Test FLOAT16 index + searchOptions16 := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)}, + } + res16, err := client.FTSearchWithArgs(ctx, "idx16", "*=>[KNN 3 @v16 $vec as score]", searchOptions16).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res16.Total).To(BeEquivalentTo(3)) + + // Test FLOAT32 index + searchOptions32 := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)}, + } + res32, err := client.FTSearchWithArgs(ctx, "idx32", "*=>[KNN 3 @v32 $vec as score]", searchOptions32).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res32.Total).To(BeEquivalentTo(3)) + }) + + It("should test VAMANA construction window size", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "L2", + ConstructionWindowSize: 300, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA graph max degree", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "COSINE", + GraphMaxDegree: 64, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 25) + for i := 0; i < 25; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 6 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(6)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA search window size", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 6, + DistanceMetric: "L2", + SearchWindowSize: 20, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 30) + for i := 0; i < 30; i++ { + vec := make([]float32, 6) + for j := 0; j < 6; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 8 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(8)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + + It("should test VAMANA all advanced parameters", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + ConstructionWindowSize: 200, + GraphMaxDegree: 32, + SearchWindowSize: 15, + Epsilon: 0.01, + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 15) + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + Expect(res.Docs[0].ID).To(BeEquivalentTo("doc0")) + }) + It("should fail when using a non-zero offset with a zero limit", Label("search", "ftsearch"), func() { SkipBeforeRedisVersion(7.9, "requires Redis 8.x") val, err := client.FTCreate(ctx, "testIdx", &redis.FTCreateOptions{}, &redis.FieldSchema{ From bd8e9b53d5e129d4900bb04f2a155f69e457a15a Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 4 Aug 2025 07:15:34 +0100 Subject: [PATCH 218/230] Improve stale issue workflow (#3458) * updated stale issue policy Signed-off-by: Elena Kolevska * Adds a temporary dry run Signed-off-by: Elena Kolevska --------- Signed-off-by: Elena Kolevska --- .github/workflows/stale-issues.yml | 106 ++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 17 deletions(-) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 445af1c818..f24d4f9321 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -1,25 +1,97 @@ -name: "Close stale issues" +name: "Stale Issue Management" on: schedule: - - cron: "0 0 * * *" + # Run daily at midnight UTC + - cron: "0 0 * * *" + workflow_dispatch: # Allow manual triggering + +env: + # Default stale policy timeframes + DAYS_BEFORE_STALE: 365 + DAYS_BEFORE_CLOSE: 30 + + # Accelerated timeline for needs-information issues + NEEDS_INFO_DAYS_BEFORE_STALE: 30 + NEEDS_INFO_DAYS_BEFORE_CLOSE: 7 -permissions: {} jobs: stale: - permissions: - issues: write # to close stale issues (actions/stale) - pull-requests: write # to close stale PRs (actions/stale) + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + dry-run: true + + # Default stale policy + days-before-stale: ${{ env.DAYS_BEFORE_STALE }} + days-before-close: ${{ env.DAYS_BEFORE_CLOSE }} + + # Explicit stale label configuration + stale-issue-label: "stale" + stale-pr-label: "stale" + + stale-issue-message: | + This issue has been automatically marked as stale due to inactivity. + It will be closed in 30 days if no further activity occurs. + If you believe this issue is still relevant, please add a comment to keep it open. + + close-issue-message: | + This issue has been automatically closed due to inactivity. + If you believe this issue is still relevant, please reopen it or create a new issue with updated information. + + # Exclude needs-information issues from this job + exempt-issue-labels: 'no-stale,needs-information' + + # Remove stale label when issue/PR becomes active again + remove-stale-when-updated: true + + # Apply to pull requests with same timeline + days-before-pr-stale: ${{ env.DAYS_BEFORE_STALE }} + days-before-pr-close: ${{ env.DAYS_BEFORE_CLOSE }} + stale-pr-message: | + This pull request has been automatically marked as stale due to inactivity. + It will be closed in 30 days if no further activity occurs. + + close-pr-message: | + This pull request has been automatically closed due to inactivity. + If you would like to continue this work, please reopen the PR or create a new one. + + # Only exclude no-stale PRs (needs-information PRs follow standard timeline) + exempt-pr-labels: 'no-stale' + + # Separate job for needs-information issues ONLY with accelerated timeline + stale-needs-info: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' - stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' - days-before-stale: 365 - days-before-close: 30 - stale-issue-label: "Stale" - stale-pr-label: "Stale" - operations-per-run: 10 - remove-stale-when-updated: true + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + dry-run: true + + # Accelerated timeline for needs-information + days-before-stale: ${{ env.NEEDS_INFO_DAYS_BEFORE_STALE }} + days-before-close: ${{ env.NEEDS_INFO_DAYS_BEFORE_CLOSE }} + + # Explicit stale label configuration + stale-issue-label: "stale" + + # Only target ISSUES with needs-information label (not PRs) + only-issue-labels: 'needs-information' + + stale-issue-message: | + This issue has been marked as stale because it requires additional information + that has not been provided for 30 days. It will be closed in 7 days if the + requested information is not provided. + + close-issue-message: | + This issue has been closed because the requested information was not provided within the specified timeframe. + If you can provide the missing information, please reopen this issue or create a new one. + + # Disable PR processing for this job + days-before-pr-stale: -1 + days-before-pr-close: -1 + + # Remove stale label when issue becomes active again + remove-stale-when-updated: true \ No newline at end of file From 44c80707369e685ea1d4fd4d115cd170a2e659a9 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:16:54 +0300 Subject: [PATCH 219/230] feat(proto): add configurable buffer sizes for Redis connections (#3453) * add configurable buffer sizes for Redis connections * add MiB to wordlist * Add description for buffer size parameter --- .github/wordlist.txt | 3 +- README.md | 12 ++ internal/pool/buffer_size_test.go | 183 ++++++++++++++++++++++++++++++ internal/pool/conn.go | 20 +++- internal/pool/pool.go | 5 +- internal/proto/reader.go | 11 +- options.go | 23 ++++ osscluster.go | 22 ++++ ring.go | 24 ++++ 9 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 internal/pool/buffer_size_test.go diff --git a/.github/wordlist.txt b/.github/wordlist.txt index a922d99bac..e0c73eb506 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -73,4 +73,5 @@ OAuth Azure StreamingCredentialsProvider oauth -entraid \ No newline at end of file +entraid +MiB \ No newline at end of file diff --git a/README.md b/README.md index c37a52ec70..356870b174 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,18 @@ func main() { ``` +### Buffer Size Configuration + +go-redis uses 0.5MiB read and write buffers by default for optimal performance. For high-throughput applications or large pipelines, you can customize buffer sizes: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + ReadBufferSize: 1024 * 1024, // 1MiB read buffer + WriteBufferSize: 1024 * 1024, // 1MiB write buffer +}) +``` + ### Advanced Configuration go-redis supports extending the client identification phase to allow projects to send their own custom client identification. diff --git a/internal/pool/buffer_size_test.go b/internal/pool/buffer_size_test.go new file mode 100644 index 0000000000..a54230102a --- /dev/null +++ b/internal/pool/buffer_size_test.go @@ -0,0 +1,183 @@ +package pool_test + +import ( + "bufio" + "context" + "net" + "unsafe" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + + "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/internal/proto" +) + +var _ = Describe("Buffer Size Configuration", func() { + var connPool *pool.ConnPool + ctx := context.Background() + + AfterEach(func() { + if connPool != nil { + connPool.Close() + } + }) + + It("should use default buffer sizes when not specified", func() { + connPool = pool.NewConnPool(&pool.Options{ + Dialer: dummyDialer, + PoolSize: 1, + PoolTimeout: 1000, + }) + + cn, err := connPool.NewConn(ctx) + Expect(err).NotTo(HaveOccurred()) + defer connPool.CloseConn(cn) + + // Check that default buffer sizes are used (0.5MiB) + writerBufSize := getWriterBufSizeUnsafe(cn) + readerBufSize := getReaderBufSizeUnsafe(cn) + + Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + }) + + It("should use custom buffer sizes when specified", func() { + customReadSize := 32 * 1024 // 32KB + customWriteSize := 64 * 1024 // 64KB + + connPool = pool.NewConnPool(&pool.Options{ + Dialer: dummyDialer, + PoolSize: 1, + PoolTimeout: 1000, + ReadBufferSize: customReadSize, + WriteBufferSize: customWriteSize, + }) + + cn, err := connPool.NewConn(ctx) + Expect(err).NotTo(HaveOccurred()) + defer connPool.CloseConn(cn) + + // Check that custom buffer sizes are used + writerBufSize := getWriterBufSizeUnsafe(cn) + readerBufSize := getReaderBufSizeUnsafe(cn) + + Expect(writerBufSize).To(Equal(customWriteSize)) + Expect(readerBufSize).To(Equal(customReadSize)) + }) + + It("should handle zero buffer sizes by using defaults", func() { + connPool = pool.NewConnPool(&pool.Options{ + Dialer: dummyDialer, + PoolSize: 1, + PoolTimeout: 1000, + ReadBufferSize: 0, // Should use default + WriteBufferSize: 0, // Should use default + }) + + cn, err := connPool.NewConn(ctx) + Expect(err).NotTo(HaveOccurred()) + defer connPool.CloseConn(cn) + + // Check that default buffer sizes are used (0.5MiB) + writerBufSize := getWriterBufSizeUnsafe(cn) + readerBufSize := getReaderBufSizeUnsafe(cn) + + Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + }) + + It("should use 0.5MiB default buffer sizes for standalone NewConn", func() { + // Test that NewConn (without pool) also uses 0.5MiB defaults + netConn := newDummyConn() + cn := pool.NewConn(netConn) + defer cn.Close() + + writerBufSize := getWriterBufSizeUnsafe(cn) + readerBufSize := getReaderBufSizeUnsafe(cn) + + Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + }) + + It("should use 0.5MiB defaults even when pool is created directly without buffer sizes", func() { + // Test the scenario where someone creates a pool directly (like in tests) + // without setting ReadBufferSize and WriteBufferSize + connPool = pool.NewConnPool(&pool.Options{ + Dialer: dummyDialer, + PoolSize: 1, + PoolTimeout: 1000, + // ReadBufferSize and WriteBufferSize are not set (will be 0) + }) + + cn, err := connPool.NewConn(ctx) + Expect(err).NotTo(HaveOccurred()) + defer connPool.CloseConn(cn) + + // Should still get 0.5MiB defaults because NewConnPool sets them + writerBufSize := getWriterBufSizeUnsafe(cn) + readerBufSize := getReaderBufSizeUnsafe(cn) + + Expect(writerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + Expect(readerBufSize).To(Equal(proto.DefaultBufferSize)) // Default 0.5MiB buffer size + }) +}) + +// Helper functions to extract buffer sizes using unsafe pointers +func getWriterBufSizeUnsafe(cn *pool.Conn) int { + cnPtr := (*struct { + usedAt int64 + netConn net.Conn + rd *proto.Reader + bw *bufio.Writer + wr *proto.Writer + // ... other fields + })(unsafe.Pointer(cn)) + + if cnPtr.bw == nil { + return -1 + } + + bwPtr := (*struct { + err error + buf []byte + n int + wr interface{} + })(unsafe.Pointer(cnPtr.bw)) + + return len(bwPtr.buf) +} + +func getReaderBufSizeUnsafe(cn *pool.Conn) int { + cnPtr := (*struct { + usedAt int64 + netConn net.Conn + rd *proto.Reader + bw *bufio.Writer + wr *proto.Writer + // ... other fields + })(unsafe.Pointer(cn)) + + if cnPtr.rd == nil { + return -1 + } + + rdPtr := (*struct { + rd *bufio.Reader + })(unsafe.Pointer(cnPtr.rd)) + + if rdPtr.rd == nil { + return -1 + } + + bufReaderPtr := (*struct { + buf []byte + rd interface{} + r, w int + err error + lastByte int + lastRuneSize int + })(unsafe.Pointer(rdPtr.rd)) + + return len(bufReaderPtr.buf) +} diff --git a/internal/pool/conn.go b/internal/pool/conn.go index c1087b401a..989ab10d23 100644 --- a/internal/pool/conn.go +++ b/internal/pool/conn.go @@ -28,12 +28,28 @@ type Conn struct { } func NewConn(netConn net.Conn) *Conn { + return NewConnWithBufferSize(netConn, proto.DefaultBufferSize, proto.DefaultBufferSize) +} + +func NewConnWithBufferSize(netConn net.Conn, readBufSize, writeBufSize int) *Conn { cn := &Conn{ netConn: netConn, createdAt: time.Now(), } - cn.rd = proto.NewReader(netConn) - cn.bw = bufio.NewWriter(netConn) + + // Use specified buffer sizes, or fall back to 0.5MiB defaults if 0 + if readBufSize > 0 { + cn.rd = proto.NewReaderSize(netConn, readBufSize) + } else { + cn.rd = proto.NewReader(netConn) // Uses 0.5MiB default + } + + if writeBufSize > 0 { + cn.bw = bufio.NewWriterSize(netConn, writeBufSize) + } else { + cn.bw = bufio.NewWriterSize(netConn, proto.DefaultBufferSize) + } + cn.wr = proto.NewWriter(cn.bw) cn.SetUsedAt(time.Now()) return cn diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 3ee3dea6d8..6d3381c9fa 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -71,6 +71,9 @@ type Options struct { MaxActiveConns int ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration + + ReadBufferSize int + WriteBufferSize int } type lastDialErrorWrap struct { @@ -226,7 +229,7 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) { return nil, err } - cn := NewConn(netConn) + cn := NewConnWithBufferSize(netConn, p.cfg.ReadBufferSize, p.cfg.WriteBufferSize) cn.pooled = pooled return cn, nil } diff --git a/internal/proto/reader.go b/internal/proto/reader.go index 8d23817fe8..a447809989 100644 --- a/internal/proto/reader.go +++ b/internal/proto/reader.go @@ -12,6 +12,9 @@ import ( "github.com/redis/go-redis/v9/internal/util" ) +// DefaultBufferSize is the default size for read/write buffers (0.5MiB) +const DefaultBufferSize = 512 * 1024 + // redis resp protocol data type. const ( RespStatus = '+' // +\r\n @@ -58,7 +61,13 @@ type Reader struct { func NewReader(rd io.Reader) *Reader { return &Reader{ - rd: bufio.NewReader(rd), + rd: bufio.NewReaderSize(rd, DefaultBufferSize), + } +} + +func NewReaderSize(rd io.Reader, size int) *Reader { + return &Reader{ + rd: bufio.NewReaderSize(rd, size), } } diff --git a/options.go b/options.go index b87a234a41..2ce807e4c6 100644 --- a/options.go +++ b/options.go @@ -15,6 +15,7 @@ import ( "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/internal/proto" ) // Limiter is the interface of a rate limiter or a circuit breaker. @@ -130,6 +131,20 @@ type Options struct { // See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts ContextTimeoutEnabled bool + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + WriteBufferSize int + // PoolFIFO type of connection pool. // // - true for FIFO pool @@ -241,6 +256,12 @@ func (opt *Options) init() { if opt.PoolSize == 0 { opt.PoolSize = 10 * runtime.GOMAXPROCS(0) } + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } switch opt.ReadTimeout { case -2: opt.ReadTimeout = -1 @@ -592,5 +613,7 @@ func newConnPool( MaxActiveConns: opt.MaxActiveConns, ConnMaxIdleTime: opt.ConnMaxIdleTime, ConnMaxLifetime: opt.ConnMaxLifetime, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, }) } diff --git a/osscluster.go b/osscluster.go index 0526022ba0..ad654821d1 100644 --- a/osscluster.go +++ b/osscluster.go @@ -92,6 +92,20 @@ type ClusterOptions struct { ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + WriteBufferSize int + TLSConfig *tls.Config // DisableIndentity - Disable set-lib on connect. @@ -127,6 +141,12 @@ func (opt *ClusterOptions) init() { if opt.PoolSize == 0 { opt.PoolSize = 5 * runtime.GOMAXPROCS(0) } + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } switch opt.ReadTimeout { case -1: @@ -318,6 +338,8 @@ func (opt *ClusterOptions) clientOptions() *Options { MaxActiveConns: opt.MaxActiveConns, ConnMaxIdleTime: opt.ConnMaxIdleTime, ConnMaxLifetime: opt.ConnMaxLifetime, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIdentity, IdentitySuffix: opt.IdentitySuffix, diff --git a/ring.go b/ring.go index 0c15660197..0d73e0101c 100644 --- a/ring.go +++ b/ring.go @@ -18,6 +18,7 @@ import ( "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" "github.com/redis/go-redis/v9/internal/pool" + "github.com/redis/go-redis/v9/internal/proto" "github.com/redis/go-redis/v9/internal/rand" ) @@ -113,6 +114,20 @@ type RingOptions struct { ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration + // ReadBufferSize is the size of the bufio.Reader buffer for each connection. + // Larger buffers can improve performance for commands that return large responses. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + ReadBufferSize int + + // WriteBufferSize is the size of the bufio.Writer buffer for each connection. + // Larger buffers can improve performance for large pipelines and commands with many arguments. + // Smaller buffers can improve memory usage for larger pools. + // + // default: 0.5MiB (524288 bytes) + WriteBufferSize int + TLSConfig *tls.Config Limiter Limiter @@ -164,6 +179,13 @@ func (opt *RingOptions) init() { case 0: opt.MaxRetryBackoff = 512 * time.Millisecond } + + if opt.ReadBufferSize == 0 { + opt.ReadBufferSize = proto.DefaultBufferSize + } + if opt.WriteBufferSize == 0 { + opt.WriteBufferSize = proto.DefaultBufferSize + } } func (opt *RingOptions) clientOptions() *Options { @@ -195,6 +217,8 @@ func (opt *RingOptions) clientOptions() *Options { MaxActiveConns: opt.MaxActiveConns, ConnMaxIdleTime: opt.ConnMaxIdleTime, ConnMaxLifetime: opt.ConnMaxLifetime, + ReadBufferSize: opt.ReadBufferSize, + WriteBufferSize: opt.WriteBufferSize, TLSConfig: opt.TLSConfig, Limiter: opt.Limiter, From c56822517a604f2ce886db11ece5519a532c442e Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:07:15 +0300 Subject: [PATCH 220/230] chore(tests): Add VAMANA compression algorithm tests (#3461) --- search_test.go | 412 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) diff --git a/search_test.go b/search_test.go index ede29c4613..f9895a111c 100644 --- a/search_test.go +++ b/search_test.go @@ -2904,6 +2904,418 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Rows[0].Fields["maxValue"]).To(BeEquivalentTo("-inf")) }) + It("should test VAMANA with LVQ4 compression", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ4", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + // Don't check specific document ID as vector search is probabilistic + Expect(res.Docs).To(HaveLen(5)) + }) + + It("should test VAMANA with LeanVec4x8 compression and reduce parameter", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LeanVec4x8", + TrainingThreshold: 1024, + ReduceDim: 4, // Reduce dimension to 4 (half of original 8) + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + // Don't check specific document ID as vector search is probabilistic + Expect(res.Docs).To(HaveLen(5)) + }) + + It("should test VAMANA compression algorithms with FLOAT16 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"} + + for _, compression := range compressionAlgorithms { + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 8, + DistanceMetric: "L2", + Compression: compression, + TrainingThreshold: 1024, + } + + // Add reduce parameter for LeanVec compressions + if strings.HasPrefix(compression, "LeanVec") { + vamanaOptions.ReduceDim = 4 + } + + indexName := fmt.Sprintf("idx_%s", compression) + val, err := client.FTCreate(ctx, indexName, + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, indexName) + + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc_%s_%d", compression, i), "v", encodeFloat16Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)}, + } + res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + } + }) + + It("should test VAMANA compression algorithms with FLOAT32 type", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"} + + for _, compression := range compressionAlgorithms { + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: compression, + TrainingThreshold: 1024, + } + + // Add reduce parameter for LeanVec compressions + if strings.HasPrefix(compression, "LeanVec") { + vamanaOptions.ReduceDim = 4 + } + + indexName := fmt.Sprintf("idx_%s", compression) + val, err := client.FTCreate(ctx, indexName, + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, indexName) + + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc_%s_%d", compression, i), "v", encodeFloat32Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)}, + } + res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + } + }) + + It("should test VAMANA compression with different distance metrics", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"} + distanceMetrics := []string{"L2", "COSINE", "IP"} + + for _, compression := range compressionAlgorithms { + for _, metric := range distanceMetrics { + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: metric, + Compression: compression, + TrainingThreshold: 1024, + } + + // Add reduce parameter for LeanVec compressions + if strings.HasPrefix(compression, "LeanVec") { + vamanaOptions.ReduceDim = 4 + } + + indexName := fmt.Sprintf("idx_%s_%s", compression, metric) + val, err := client.FTCreate(ctx, indexName, + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, indexName) + + for i := 0; i < 10; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc_%s_%s_%d", compression, metric, i), "v", encodeFloat32Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)}, + } + res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + } + } + }) + + It("should test VAMANA compression with all advanced parameters", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"} + + for _, compression := range compressionAlgorithms { + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: compression, + ConstructionWindowSize: 200, + GraphMaxDegree: 32, + SearchWindowSize: 15, + Epsilon: 0.01, + TrainingThreshold: 1024, + } + + // Add reduce parameter for LeanVec compressions + if strings.HasPrefix(compression, "LeanVec") { + vamanaOptions.ReduceDim = 4 + } + + indexName := fmt.Sprintf("idx_%s_advanced", compression) + val, err := client.FTCreate(ctx, indexName, + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, indexName) + + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc_%s_advanced_%d", compression, i), "v", encodeFloat32Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(queryVec)}, + } + res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + } + }) + + It("should fail when using reduce parameter with non-LeanVec compression", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ8", + TrainingThreshold: 1024, + ReduceDim: 4, // This should fail for LVQ8 + } + _, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).To(HaveOccurred()) + }) + + It("should test VAMANA with LVQ4 compression in RESP3", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LVQ4", + TrainingThreshold: 1024, + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + // Don't check specific document ID as vector search is probabilistic + Expect(res.Docs).To(HaveLen(5)) + }) + + It("should test VAMANA with LeanVec4x8 compression and reduce parameter in RESP3", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT32", + Dim: 8, + DistanceMetric: "L2", + Compression: "LeanVec4x8", + TrainingThreshold: 1024, + ReduceDim: 4, // Reduce dimension to 4 (half of original 8) + } + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + vectors := make([][]float32, 20) + for i := 0; i < 20; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + vectors[i] = vec + client.HSet(ctx, fmt.Sprintf("doc%d", i), "v", encodeFloat32Vector(vec)) + } + + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat32Vector(vectors[0])}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 5 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(5)) + // Don't check specific document ID as vector search is probabilistic + Expect(res.Docs).To(HaveLen(5)) + }) + + It("should test VAMANA compression algorithms with FLOAT16 type in RESP3", Label("search", "ftcreate", "vamana"), func() { + SkipBeforeRedisVersion(8.2, "VAMANA requires Redis 8.2+") + + compressionAlgorithms := []string{"LVQ4", "LVQ4x4", "LVQ4x8", "LeanVec4x8", "LeanVec8x8"} + + for _, compression := range compressionAlgorithms { + vamanaOptions := &redis.FTVamanaOptions{ + Type: "FLOAT16", + Dim: 8, + DistanceMetric: "L2", + Compression: compression, + TrainingThreshold: 1024, + } + + // Add reduce parameter for LeanVec compressions + if strings.HasPrefix(compression, "LeanVec") { + vamanaOptions.ReduceDim = 4 + } + + indexName := fmt.Sprintf("idx_resp3_%s", compression) + val, err := client.FTCreate(ctx, indexName, + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{VamanaOptions: vamanaOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, indexName) + + // Add test data + for i := 0; i < 15; i++ { + vec := make([]float32, 8) + for j := 0; j < 8; j++ { + vec[j] = float32(i + j) + } + client.HSet(ctx, fmt.Sprintf("doc_resp3_%s_%d", compression, i), "v", encodeFloat16Vector(vec)) + } + + queryVec := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} + searchOptions := &redis.FTSearchOptions{ + DialectVersion: 2, + NoContent: true, + Params: map[string]interface{}{"vec": encodeFloat16Vector(queryVec)}, + } + res, err := client.FTSearchWithArgs(ctx, indexName, "*=>[KNN 3 @v $vec as score]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeEquivalentTo(3)) + } + }) }) func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { From 5548714b6ccdf0619d63295b7198b523925e66fd Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:08:10 +0100 Subject: [PATCH 221/230] chore(doc): DOC-5472 time series doc examples (#3443) * DOC-5472 added and fixed tests up to * DOC-5472 added agg bucket examples * DOC-5472 time series doc examples * DOC-5472 removed black lines above error checks, following feedback * DOC-5472 fixed param formatting, following feedback --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- doctests/timeseries_tut_test.go | 1163 +++++++++++++++++++++++++++++++ 1 file changed, 1163 insertions(+) create mode 100644 doctests/timeseries_tut_test.go diff --git a/doctests/timeseries_tut_test.go b/doctests/timeseries_tut_test.go new file mode 100644 index 0000000000..28e1635399 --- /dev/null +++ b/doctests/timeseries_tut_test.go @@ -0,0 +1,1163 @@ +// EXAMPLE: time_series_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "maps" + "math" + "slices" + "sort" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_timeseries_create() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "thermometer:1", "thermometer:2", "thermometer:3") + // REMOVE_END + + // STEP_START create + res1, err := rdb.TSCreate(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.Type(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> TSDB-TYPE + + res3, err := rdb.TSInfo(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res3["totalSamples"]) // >>> 0 + // STEP_END + + // STEP_START create_retention + res4, err := rdb.TSAddWithArgs( + ctx, + "thermometer:2", + 1, + 10.8, + &redis.TSOptions{ + Retention: 100, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> 1 + + res5, err := rdb.TSInfo(ctx, "thermometer:2").Result() + if err != nil { + panic(err) + } + + fmt.Println(res5["retentionTime"]) // >>> 100 + // STEP_END + + // STEP_START create_labels + res6, err := rdb.TSAddWithArgs( + ctx, + "thermometer:3", + 1, + 10.4, + &redis.TSOptions{ + Labels: map[string]string{ + "location": "UK", + "type": "Mercury", + }, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> 1 + + res7, err := rdb.TSInfo(ctx, "thermometer:3").Result() + if err != nil { + panic(err) + } + + fmt.Println(res7["labels"]) + // >>> map[location:UK type:Mercury] + // STEP_END + + // Output: + // OK + // TSDB-TYPE + // 0 + // 1 + // 100 + // 1 + // map[location:UK type:Mercury] +} + +func ExampleClient_timeseries_add() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "thermometer:1", "thermometer:2") + rdb.TSCreate(ctx, "thermometer:1") + rdb.TSCreate(ctx, "thermometer:2") + // REMOVE_END + + // STEP_START madd + res1, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"thermometer:1", 1, 9.2}, + {"thermometer:1", 2, 9.9}, + {"thermometer:2", 2, 10.3}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> [1 2 2] + // STEP_END + + // STEP_START get + // The last recorded temperature for thermometer:2 + // was 10.3 at time 2. + res2, err := rdb.TSGet(ctx, "thermometer:2").Result() + if err != nil { + panic(err) + } + + fmt.Println(res2) + // >>> {2 10.3} + // STEP_END + + // Output: + // [1 2 2] + // {2 10.3} +} + +func ExampleClient_timeseries_range() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "rg:1") + // REMOVE_END + + // STEP_START range + // Add 5 data points to a time series named "rg:1". + res1, err := rdb.TSCreate(ctx, "rg:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:1", 0, 18}, + {"rg:1", 1, 14}, + {"rg:1", 2, 22}, + {"rg:1", 3, 18}, + {"rg:1", 4, 24}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [0 1 2 3 4] + + // Retrieve all the data points in ascending order. + // Note: use 0 and `math.MaxInt64` instead of - and + + // to denote the minimum and maximum possible timestamps. + res3, err := rdb.TSRange(ctx, "rg:1", 0, math.MaxInt64).Result() + if err != nil { + panic(err) + } + + fmt.Println(res3) + // >>> [{0 18} {1 14} {2 22} {3 18} {4 24}] + + // Retrieve data points up to time 1 (inclusive). + res4, err := rdb.TSRange(ctx, "rg:1", 0, 1).Result() + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> [{0 18} {1 14}] + + // Retrieve data points from time 3 onwards. + res5, err := rdb.TSRange(ctx, "rg:1", 3, math.MaxInt64).Result() + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> [{3 18} {4 24}] + + // Retrieve all the data points in descending order. + res6, err := rdb.TSRevRange(ctx, "rg:1", 0, math.MaxInt64).Result() + if err != nil { + panic(err) + } + + fmt.Println(res6) + // >>> [{4 24} {3 18} {2 22} {1 14} {0 18}] + + // Retrieve data points up to time 1 (inclusive), but return them + // in descending order. + res7, err := rdb.TSRevRange(ctx, "rg:1", 0, 1).Result() + if err != nil { + panic(err) + } + + fmt.Println(res7) + // >>> [{1 14} {0 18}] + // STEP_END + + // STEP_START range_filter + res8, err := rdb.TSRangeWithArgs( + ctx, + "rg:1", + 0, + math.MaxInt64, + &redis.TSRangeOptions{ + FilterByTS: []int{0, 2, 4}, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> [{0 18} {2 22} {4 24}] + + res9, err := rdb.TSRevRangeWithArgs( + ctx, + "rg:1", + 0, + math.MaxInt64, + &redis.TSRevRangeOptions{ + FilterByTS: []int{0, 2, 4}, + FilterByValue: []int{20, 25}, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [{4 24} {2 22}] + + res10, err := rdb.TSRevRangeWithArgs( + ctx, + "rg:1", + 0, + math.MaxInt64, + &redis.TSRevRangeOptions{ + FilterByTS: []int{0, 2, 4}, + FilterByValue: []int{22, 22}, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> [{2 22}] + // STEP_END + + // Output: + // OK + // [0 1 2 3 4] + // [{0 18} {1 14} {2 22} {3 18} {4 24}] + // [{0 18} {1 14}] + // [{3 18} {4 24}] + // [{4 24} {3 18} {2 22} {1 14} {0 18}] + // [{1 14} {0 18}] + // [{0 18} {2 22} {4 24}] + // [{4 24} {2 22}] + // [{2 22}] +} + +func ExampleClient_timeseries_query_multi() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "rg:2", "rg:3", "rg:4") + // REMOVE_END + + // STEP_START query_multi + // Create three new "rg:" time series (two in the US + // and one in the UK, with different units) and add some + // data points. + res20, err := rdb.TSCreateWithArgs(ctx, "rg:2", &redis.TSOptions{ + Labels: map[string]string{"location": "us", "unit": "cm"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> OK + + res21, err := rdb.TSCreateWithArgs(ctx, "rg:3", &redis.TSOptions{ + Labels: map[string]string{"location": "us", "unit": "in"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> OK + + res22, err := rdb.TSCreateWithArgs(ctx, "rg:4", &redis.TSOptions{ + Labels: map[string]string{"location": "uk", "unit": "mm"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> OK + + res23, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 0, 1.8}, + {"rg:3", 0, 0.9}, + {"rg:4", 0, 25}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> [0 0 0] + + res24, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 1, 2.1}, + {"rg:3", 1, 0.77}, + {"rg:4", 1, 18}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res24) // >>> [1 1 1] + + res25, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 2, 2.3}, + {"rg:3", 2, 1.1}, + {"rg:4", 2, 21}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res25) // >>> [2 2 2] + + res26, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 3, 1.9}, + {"rg:3", 3, 0.81}, + {"rg:4", 3, 19}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res26) // >>> [3 3 3] + + res27, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 4, 1.78}, + {"rg:3", 4, 0.74}, + {"rg:4", 4, 23}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> [4 4 4] + + // Retrieve the last data point from each US time series. + res28, err := rdb.TSMGet(ctx, []string{"location=us"}).Result() + if err != nil { + panic(err) + } + + res28Keys := slices.Collect(maps.Keys(res28)) + sort.Strings(res28Keys) + + for _, k := range res28Keys { + labels := res28[k][0].(map[interface{}]interface{}) + + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" %v\n", res28[k][1]) + } + // >>> rg:2: + // >>> {4 1.78} + // >>> rg:3: + // >>> {4 0.74} + + // Retrieve the same data points, but include the `unit` + // label in the results. + res29, err := rdb.TSMGetWithArgs( + ctx, + []string{"location=us"}, + &redis.TSMGetOptions{ + SelectedLabels: []interface{}{"unit"}, + }, + ).Result() + if err != nil { + panic(err) + } + + res29Keys := slices.Collect(maps.Keys(res29)) + sort.Strings(res29Keys) + + for _, k := range res29Keys { + labels := res29[k][0].(map[interface{}]interface{}) + + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" %v\n", res29[k][1]) + } + + // >>> rg:2: + // >>> unit: cm + // >>> [4 1.78] + // >>> rg:3: + // >>> unit: in + // >>> [4 0.74] + + // Retrieve data points up to time 2 (inclusive) from all + // time series that use millimeters as the unit. Include all + // labels in the results. + // Note that the `aggregators` field is empty if you don't + // specify any aggregators. + res30, err := rdb.TSMRangeWithArgs( + ctx, + 0, + 2, + []string{"unit=mm"}, + &redis.TSMRangeOptions{ + WithLabels: true, + }, + ).Result() + if err != nil { + panic(err) + } + + res30Keys := slices.Collect(maps.Keys(res30)) + sort.Strings(res30Keys) + + for _, k := range res30Keys { + labels := res30[k][0].(map[interface{}]interface{}) + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" Aggregators: %v\n", res30[k][1]) + fmt.Printf(" %v\n", res30[k][2]) + } + // >>> rg:4: + // >>> location: uk + // >>> unit: mm + // >>> Aggregators: map[aggregators:[]] + // >>> [{0 25} {1 18} {2 21}] + + // Retrieve data points from time 1 to time 3 (inclusive) from + // all time series that use centimeters or millimeters as the unit, + // but only return the `location` label. Return the results + // in descending order of timestamp. + res31, err := rdb.TSMRevRangeWithArgs( + ctx, + 1, + 3, + []string{"unit=(cm,mm)"}, + &redis.TSMRevRangeOptions{ + SelectedLabels: []interface{}{"location"}, + }, + ).Result() + if err != nil { + panic(err) + } + + res31Keys := slices.Collect(maps.Keys(res31)) + sort.Strings(res31Keys) + + for _, k := range res31Keys { + labels := res31[k][0].(map[interface{}]interface{}) + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" Aggregators: %v\n", res31[k][1]) + fmt.Printf(" %v\n", res31[k][2]) + } + // >>> rg:2: + // >>> location: us + // >>> Aggregators: map[aggregators:[]] + // >>> [{3 1.9} {2 2.3} {1 2.1}] + // >>> rg:4: + // >>> location: uk + // >>> Aggregators: map[aggregators:[]] + // >>> [{3 19} {2 21} {1 18}] + // STEP_END + + // Output: + // OK + // OK + // OK + // [0 0 0] + // [1 1 1] + // [2 2 2] + // [3 3 3] + // [4 4 4] + // rg:2: + // [4 1.78] + // rg:3: + // [4 0.74] + // rg:2: + // unit: cm + // [4 1.78] + // rg:3: + // unit: in + // [4 0.74] + // rg:4: + // location: uk + // unit: mm + // Aggregators: map[aggregators:[]] + // [[0 25] [1 18] [2 21]] + // rg:2: + // location: us + // Aggregators: map[aggregators:[]] + // [[3 1.9] [2 2.3] [1 2.1]] + // rg:4: + // location: uk + // Aggregators: map[aggregators:[]] + // [[3 19] [2 21] [1 18]] +} + +func ExampleClient_timeseries_aggregation() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "rg:2") + // REMOVE_END + + // Setup data for aggregation example + _, err := rdb.TSCreateWithArgs(ctx, "rg:2", &redis.TSOptions{ + Labels: map[string]string{"location": "us", "unit": "cm"}, + }).Result() + if err != nil { + panic(err) + } + + _, err = rdb.TSMAdd(ctx, [][]interface{}{ + {"rg:2", 0, 1.8}, + {"rg:2", 1, 2.1}, + {"rg:2", 2, 2.3}, + {"rg:2", 3, 1.9}, + {"rg:2", 4, 1.78}, + }).Result() + if err != nil { + panic(err) + } + + // STEP_START agg + res32, err := rdb.TSRangeWithArgs( + ctx, + "rg:2", + 0, + math.MaxInt64, + &redis.TSRangeOptions{ + Aggregator: redis.Avg, + BucketDuration: 2, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res32) + // >>> [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}] + // STEP_END + + // Output: + // [{0 1.9500000000000002} {2 2.0999999999999996} {4 1.78}] +} +func ExampleClient_timeseries_agg_bucket() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "sensor3") + // REMOVE_END + + // STEP_START agg_bucket + res1, err := rdb.TSCreate(ctx, "sensor3").Result() + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"sensor3", 10, 1000}, + {"sensor3", 20, 2000}, + {"sensor3", 30, 3000}, + {"sensor3", 40, 4000}, + {"sensor3", 50, 5000}, + {"sensor3", 60, 6000}, + {"sensor3", 70, 7000}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [10 20 30 40 50 60 70] + + res3, err := rdb.TSRangeWithArgs( + ctx, + "sensor3", + 10, + 70, + &redis.TSRangeOptions{ + Aggregator: redis.Min, + BucketDuration: 25, + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [{0 1000} {25 3000} {50 5000}] + // STEP_END + + // STEP_START agg_align + res4, err := rdb.TSRangeWithArgs( + ctx, + "sensor3", + 10, + 70, + &redis.TSRangeOptions{ + Aggregator: redis.Min, + BucketDuration: 25, + Align: "START", + }, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [{10 1000} {35 4000} {60 6000}] + // STEP_END + + // Output: + // OK + // [10 20 30 40 50 60 70] + // [{0 1000} {25 3000} {50 5000}] + // [{10 1000} {35 4000} {60 6000}] +} + +func ExampleClient_timeseries_aggmulti() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "wind:1", "wind:2", "wind:3", "wind:4") + // REMOVE_END + + // STEP_START agg_multi + res37, err := rdb.TSCreateWithArgs(ctx, "wind:1", &redis.TSOptions{ + Labels: map[string]string{"country": "uk"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res37) // >>> OK + + res38, err := rdb.TSCreateWithArgs(ctx, "wind:2", &redis.TSOptions{ + Labels: map[string]string{"country": "uk"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res38) // >>> OK + + res39, err := rdb.TSCreateWithArgs(ctx, "wind:3", &redis.TSOptions{ + Labels: map[string]string{"country": "us"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res39) // >>> OK + + res40, err := rdb.TSCreateWithArgs(ctx, "wind:4", &redis.TSOptions{ + Labels: map[string]string{"country": "us"}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res40) // >>> OK + + res41, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"wind:1", 1, 12}, + {"wind:2", 1, 18}, + {"wind:3", 1, 5}, + {"wind:4", 1, 20}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res41) // >>> [1 1 1 1] + + res42, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"wind:1", 2, 14}, + {"wind:2", 2, 21}, + {"wind:3", 2, 4}, + {"wind:4", 2, 25}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res42) // >>> [2 2 2 2] + + res43, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"wind:1", 3, 10}, + {"wind:2", 3, 24}, + {"wind:3", 3, 8}, + {"wind:4", 3, 18}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res43) // >>> [3 3 3 3] + + // The result pairs contain the timestamp and the maximum sample value + // for the country at that timestamp. + res44, err := rdb.TSMRangeWithArgs( + ctx, + 0, + math.MaxInt64, + []string{"country=(us,uk)"}, + &redis.TSMRangeOptions{ + GroupByLabel: "country", + Reducer: "max", + }, + ).Result() + if err != nil { + panic(err) + } + + res44Keys := slices.Collect(maps.Keys(res44)) + sort.Strings(res44Keys) + + for _, k := range res44Keys { + labels := res44[k][0].(map[interface{}]interface{}) + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" %v\n", res44[k][1]) + fmt.Printf(" %v\n", res44[k][2]) + fmt.Printf(" %v\n", res44[k][3]) + } + // >>> country=uk: + // >>> map[reducers:[max]] + // >>> map[sources:[wind:1 wind:2]] + // >>> [[1 18] [2 21] [3 24]] + // >>> country=us: + // >>> map[reducers:[max]] + // >>> map[sources:[wind:3 wind:4]] + // >>> [[1 20] [2 25] [3 18]] + + // The result pairs contain the timestamp and the average sample value + // for the country at that timestamp. + res45, err := rdb.TSMRangeWithArgs( + ctx, + 0, + math.MaxInt64, + []string{"country=(us,uk)"}, + &redis.TSMRangeOptions{ + GroupByLabel: "country", + Reducer: "avg", + }, + ).Result() + if err != nil { + panic(err) + } + + res45Keys := slices.Collect(maps.Keys(res45)) + sort.Strings(res45Keys) + + for _, k := range res45Keys { + labels := res45[k][0].(map[interface{}]interface{}) + labelKeys := make([]string, 0, len(labels)) + + for lk := range labels { + labelKeys = append(labelKeys, lk.(string)) + } + + sort.Strings(labelKeys) + + fmt.Printf("%v:\n", k) + + for _, lk := range labelKeys { + fmt.Printf(" %v: %v\n", lk, labels[lk]) + } + + fmt.Printf(" %v\n", res45[k][1]) + fmt.Printf(" %v\n", res45[k][2]) + fmt.Printf(" %v\n", res45[k][3]) + } + // >>> country=uk: + // >>> map[reducers:[avg]] + // >>> map[sources:[wind:1 wind:2]] + // >>> [[1 15] [2 17.5] [3 17]] + // >>> country=us: + // >>> map[reducers:[avg]] + // >>> map[sources:[wind:3 wind:4]] + // >>> [[1 12.5] [2 14.5] [3 13]] + // STEP_END + + // Output: + // OK + // OK + // OK + // OK + // [1 1 1 1] + // [2 2 2 2] + // [3 3 3 3] + // country=uk: + // map[reducers:[max]] + // map[sources:[wind:1 wind:2]] + // [[1 18] [2 21] [3 24]] + // country=us: + // map[reducers:[max]] + // map[sources:[wind:3 wind:4]] + // [[1 20] [2 25] [3 18]] + // country=uk: + // map[reducers:[avg]] + // map[sources:[wind:1 wind:2]] + // [[1 15] [2 17.5] [3 17]] + // country=us: + // map[reducers:[avg]] + // map[sources:[wind:3 wind:4]] + // [[1 12.5] [2 14.5] [3 13]] +} + +func ExampleClient_timeseries_compaction() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "hyg:1", "hyg:compacted") + // REMOVE_END + + // STEP_START create_compaction + res45, err := rdb.TSCreate(ctx, "hyg:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res45) // >>> OK + + res46, err := rdb.TSCreate(ctx, "hyg:compacted").Result() + if err != nil { + panic(err) + } + + fmt.Println(res46) // >>> OK + + res47, err := rdb.TSCreateRule( + ctx, "hyg:1", "hyg:compacted", redis.Min, 3, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res47) // >>> OK + + res48, err := rdb.TSInfo(ctx, "hyg:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res48["rules"]) // >>> [[hyg:compacted 3 MIN 0]] + + res49, err := rdb.TSInfo(ctx, "hyg:compacted").Result() + if err != nil { + panic(err) + } + + fmt.Println(res49["sourceKey"]) // >>> hyg:1 + // STEP_END + + // STEP_START comp_add + res50, err := rdb.TSMAdd(ctx, [][]interface{}{ + {"hyg:1", 0, 75}, + {"hyg:1", 1, 77}, + {"hyg:1", 2, 78}, + }).Result() + if err != nil { + panic(err) + } + + fmt.Println(res50) // >>> [0 1 2] + + res51, err := rdb.TSRange( + ctx, "hyg:compacted", 0, math.MaxInt64, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res51) // >>> [] + + res52, err := rdb.TSAdd(ctx, "hyg:1", 3, 79).Result() + if err != nil { + panic(err) + } + + fmt.Println(res52) // >>> 3 + + res53, err := rdb.TSRange( + ctx, "hyg:compacted", 0, math.MaxInt64, + ).Result() + if err != nil { + panic(err) + } + + fmt.Println(res53) // >>> [{0 75}] + // STEP_END + + // Output: + // OK + // OK + // OK + // map[hyg:compacted:[3 MIN 0]] + // hyg:1 + // [0 1 2] + // [] + // 3 + // [{0 75}] +} + +func ExampleClient_timeseries_delete() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password set + DB: 0, // use default DB + }) + + // REMOVE_START + // make sure we are working with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "thermometer:1") + // Setup initial data + rdb.TSCreate(ctx, "thermometer:1") + rdb.TSMAdd(ctx, [][]interface{}{ + {"thermometer:1", 1, 9.2}, + {"thermometer:1", 2, 9.9}, + }) + // REMOVE_END + + // STEP_START del + res54, err := rdb.TSInfo(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res54["totalSamples"]) // >>> 2 + fmt.Println(res54["firstTimestamp"]) // >>> 1 + fmt.Println(res54["lastTimestamp"]) // >>> 2 + + res55, err := rdb.TSAdd(ctx, "thermometer:1", 3, 9.7).Result() + if err != nil { + panic(err) + } + + fmt.Println(res55) // >>> 3 + + res56, err := rdb.TSInfo(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res56["totalSamples"]) // >>> 3 + fmt.Println(res56["firstTimestamp"]) // >>> 1 + fmt.Println(res56["lastTimestamp"]) // >>> 3 + + res57, err := rdb.TSDel(ctx, "thermometer:1", 1, 2).Result() + if err != nil { + panic(err) + } + + fmt.Println(res57) // >>> 2 + + res58, err := rdb.TSInfo(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res58["totalSamples"]) // >>> 1 + fmt.Println(res58["firstTimestamp"]) // >>> 3 + fmt.Println(res58["lastTimestamp"]) // >>> 3 + + res59, err := rdb.TSDel(ctx, "thermometer:1", 3, 3).Result() + if err != nil { + panic(err) + } + + fmt.Println(res59) // >>> 1 + + res60, err := rdb.TSInfo(ctx, "thermometer:1").Result() + if err != nil { + panic(err) + } + + fmt.Println(res60["totalSamples"]) // >>> 0 + // STEP_END + + // Output: + // 2 + // 1 + // 2 + // 3 + // 3 + // 1 + // 3 + // 2 + // 1 + // 3 + // 3 + // 1 + // 0 +} From c6868653d35d42df79afc261b1d5cd8439095bc8 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:15:44 +0300 Subject: [PATCH 222/230] feat(search): Add Query Builder for RediSearch commands (#3436) * Add search module builders and tests (#1) * Add search module builders and tests * Add tests * Use builders and Actions in more clean way * Update search_builders.go Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * Update search_builders.go Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- search_builders.go | 825 ++++++++++++++++++++++++++++++++++++++++ search_builders_test.go | 680 +++++++++++++++++++++++++++++++++ 2 files changed, 1505 insertions(+) create mode 100644 search_builders.go create mode 100644 search_builders_test.go diff --git a/search_builders.go b/search_builders.go new file mode 100644 index 0000000000..91f0634041 --- /dev/null +++ b/search_builders.go @@ -0,0 +1,825 @@ +package redis + +import ( + "context" +) + +// ---------------------- +// Search Module Builders +// ---------------------- + +// SearchBuilder provides a fluent API for FT.SEARCH +// (see original FTSearchOptions for all options). +// EXPERIMENTAL: this API is subject to change, use with caution. +type SearchBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSearchOptions +} + +// NewSearchBuilder creates a new SearchBuilder for FT.SEARCH commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSearchBuilder(ctx context.Context, index, query string) *SearchBuilder { + b := &SearchBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSearchOptions{LimitOffset: -1}} + return b +} + +// WithScores includes WITHSCORES. +func (b *SearchBuilder) WithScores() *SearchBuilder { + b.options.WithScores = true + return b +} + +// NoContent includes NOCONTENT. +func (b *SearchBuilder) NoContent() *SearchBuilder { b.options.NoContent = true; return b } + +// Verbatim includes VERBATIM. +func (b *SearchBuilder) Verbatim() *SearchBuilder { b.options.Verbatim = true; return b } + +// NoStopWords includes NOSTOPWORDS. +func (b *SearchBuilder) NoStopWords() *SearchBuilder { b.options.NoStopWords = true; return b } + +// WithPayloads includes WITHPAYLOADS. +func (b *SearchBuilder) WithPayloads() *SearchBuilder { + b.options.WithPayloads = true + return b +} + +// WithSortKeys includes WITHSORTKEYS. +func (b *SearchBuilder) WithSortKeys() *SearchBuilder { + b.options.WithSortKeys = true + return b +} + +// Filter adds a FILTER clause: FILTER . +func (b *SearchBuilder) Filter(field string, min, max interface{}) *SearchBuilder { + b.options.Filters = append(b.options.Filters, FTSearchFilter{ + FieldName: field, + Min: min, + Max: max, + }) + return b +} + +// GeoFilter adds a GEOFILTER clause: GEOFILTER . +func (b *SearchBuilder) GeoFilter(field string, lon, lat, radius float64, unit string) *SearchBuilder { + b.options.GeoFilter = append(b.options.GeoFilter, FTSearchGeoFilter{ + FieldName: field, + Longitude: lon, + Latitude: lat, + Radius: radius, + Unit: unit, + }) + return b +} + +// InKeys restricts the search to the given keys. +func (b *SearchBuilder) InKeys(keys ...interface{}) *SearchBuilder { + b.options.InKeys = append(b.options.InKeys, keys...) + return b +} + +// InFields restricts the search to the given fields. +func (b *SearchBuilder) InFields(fields ...interface{}) *SearchBuilder { + b.options.InFields = append(b.options.InFields, fields...) + return b +} + +// ReturnFields adds simple RETURN ... +func (b *SearchBuilder) ReturnFields(fields ...string) *SearchBuilder { + for _, f := range fields { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: f}) + } + return b +} + +// ReturnAs adds RETURN AS . +func (b *SearchBuilder) ReturnAs(field, alias string) *SearchBuilder { + b.options.Return = append(b.options.Return, FTSearchReturn{FieldName: field, As: alias}) + return b +} + +// Slop adds SLOP . +func (b *SearchBuilder) Slop(slop int) *SearchBuilder { + b.options.Slop = slop + return b +} + +// Timeout adds TIMEOUT . +func (b *SearchBuilder) Timeout(timeout int) *SearchBuilder { + b.options.Timeout = timeout + return b +} + +// InOrder includes INORDER. +func (b *SearchBuilder) InOrder() *SearchBuilder { + b.options.InOrder = true + return b +} + +// Language sets LANGUAGE . +func (b *SearchBuilder) Language(lang string) *SearchBuilder { + b.options.Language = lang + return b +} + +// Expander sets EXPANDER . +func (b *SearchBuilder) Expander(expander string) *SearchBuilder { + b.options.Expander = expander + return b +} + +// Scorer sets SCORER . +func (b *SearchBuilder) Scorer(scorer string) *SearchBuilder { + b.options.Scorer = scorer + return b +} + +// ExplainScore includes EXPLAINSCORE. +func (b *SearchBuilder) ExplainScore() *SearchBuilder { + b.options.ExplainScore = true + return b +} + +// Payload sets PAYLOAD . +func (b *SearchBuilder) Payload(payload string) *SearchBuilder { + b.options.Payload = payload + return b +} + +// SortBy adds SORTBY ASC|DESC. +func (b *SearchBuilder) SortBy(field string, asc bool) *SearchBuilder { + b.options.SortBy = append(b.options.SortBy, FTSearchSortBy{ + FieldName: field, + Asc: asc, + Desc: !asc, + }) + return b +} + +// WithSortByCount includes WITHCOUNT (when used with SortBy). +func (b *SearchBuilder) WithSortByCount() *SearchBuilder { + b.options.SortByWithCount = true + return b +} + +// Param adds a single PARAMS . +func (b *SearchBuilder) Param(key string, value interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, 1) + } + b.options.Params[key] = value + return b +} + +// ParamsMap adds multiple PARAMS at once. +func (b *SearchBuilder) ParamsMap(p map[string]interface{}) *SearchBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *SearchBuilder) Dialect(version int) *SearchBuilder { + b.options.DialectVersion = version + return b +} + +// Limit sets OFFSET and COUNT. CountOnly uses LIMIT 0 0. +func (b *SearchBuilder) Limit(offset, count int) *SearchBuilder { + b.options.LimitOffset = offset + b.options.Limit = count + return b +} +func (b *SearchBuilder) CountOnly() *SearchBuilder { b.options.CountOnly = true; return b } + +// Run executes FT.SEARCH and returns a typed result. +func (b *SearchBuilder) Run() (FTSearchResult, error) { + cmd := b.c.FTSearchWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// AggregateBuilder for FT.AGGREGATE +// ---------------------- + +type AggregateBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTAggregateOptions +} + +// NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) *AggregateBuilder { + return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} +} + +// Verbatim includes VERBATIM. +func (b *AggregateBuilder) Verbatim() *AggregateBuilder { b.options.Verbatim = true; return b } + +// AddScores includes ADDSCORES. +func (b *AggregateBuilder) AddScores() *AggregateBuilder { b.options.AddScores = true; return b } + +// Scorer sets SCORER . +func (b *AggregateBuilder) Scorer(s string) *AggregateBuilder { + b.options.Scorer = s + return b +} + +// LoadAll includes LOAD * (mutually exclusive with Load). +func (b *AggregateBuilder) LoadAll() *AggregateBuilder { + b.options.LoadAll = true + return b +} + +// Load adds LOAD [AS alias]... +// You can call it multiple times for multiple fields. +func (b *AggregateBuilder) Load(field string, alias ...string) *AggregateBuilder { + // each Load entry becomes one element in options.Load + l := FTAggregateLoad{Field: field} + if len(alias) > 0 { + l.As = alias[0] + } + b.options.Load = append(b.options.Load, l) + return b +} + +// Timeout sets TIMEOUT . +func (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder { + b.options.Timeout = ms + return b +} + +// Apply adds APPLY [AS alias]. +func (b *AggregateBuilder) Apply(field string, alias ...string) *AggregateBuilder { + a := FTAggregateApply{Field: field} + if len(alias) > 0 { + a.As = alias[0] + } + b.options.Apply = append(b.options.Apply, a) + return b +} + +// GroupBy starts a new GROUPBY clause. +func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder { + b.options.GroupBy = append(b.options.GroupBy, FTAggregateGroupBy{ + Fields: fields, + }) + return b +} + +// Reduce adds a REDUCE [<#args> ] clause to the *last* GROUPBY. +func (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder { + if len(b.options.GroupBy) == 0 { + // no GROUPBY yet — nothing to attach to + return b + } + idx := len(b.options.GroupBy) - 1 + b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ + Reducer: fn, + Args: args, + }) + return b +} + +// ReduceAs does the same but also sets an alias: REDUCE … AS +func (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder { + if len(b.options.GroupBy) == 0 { + return b + } + idx := len(b.options.GroupBy) - 1 + b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ + Reducer: fn, + Args: args, + As: alias, + }) + return b +} + +// SortBy adds SORTBY ASC|DESC. +func (b *AggregateBuilder) SortBy(field string, asc bool) *AggregateBuilder { + sb := FTAggregateSortBy{FieldName: field, Asc: asc, Desc: !asc} + b.options.SortBy = append(b.options.SortBy, sb) + return b +} + +// SortByMax sets MAX (only if SortBy was called). +func (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder { + b.options.SortByMax = max + return b +} + +// Filter sets FILTER . +func (b *AggregateBuilder) Filter(expr string) *AggregateBuilder { + b.options.Filter = expr + return b +} + +// WithCursor enables WITHCURSOR [COUNT ] [MAXIDLE ]. +func (b *AggregateBuilder) WithCursor(count, maxIdle int) *AggregateBuilder { + b.options.WithCursor = true + if b.options.WithCursorOptions == nil { + b.options.WithCursorOptions = &FTAggregateWithCursor{} + } + b.options.WithCursorOptions.Count = count + b.options.WithCursorOptions.MaxIdle = maxIdle + return b +} + +// Params adds PARAMS pairs. +func (b *AggregateBuilder) Params(p map[string]interface{}) *AggregateBuilder { + if b.options.Params == nil { + b.options.Params = make(map[string]interface{}, len(p)) + } + for k, v := range p { + b.options.Params[k] = v + } + return b +} + +// Dialect sets DIALECT . +func (b *AggregateBuilder) Dialect(version int) *AggregateBuilder { + b.options.DialectVersion = version + return b +} + +// Run executes FT.AGGREGATE and returns a typed result. +func (b *AggregateBuilder) Run() (*FTAggregateResult, error) { + cmd := b.c.FTAggregateWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// CreateIndexBuilder for FT.CREATE +// ---------------------- +// CreateIndexBuilder is builder for FT.CREATE +// EXPERIMENTAL: this API is subject to change, use with caution. +type CreateIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTCreateOptions + schema []*FieldSchema +} + +// NewCreateIndexBuilder creates a new CreateIndexBuilder for FT.CREATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewCreateIndexBuilder(ctx context.Context, index string) *CreateIndexBuilder { + return &CreateIndexBuilder{c: c, ctx: ctx, index: index, options: &FTCreateOptions{}} +} + +// OnHash sets ON HASH. +func (b *CreateIndexBuilder) OnHash() *CreateIndexBuilder { b.options.OnHash = true; return b } + +// OnJSON sets ON JSON. +func (b *CreateIndexBuilder) OnJSON() *CreateIndexBuilder { b.options.OnJSON = true; return b } + +// Prefix sets PREFIX. +func (b *CreateIndexBuilder) Prefix(prefixes ...interface{}) *CreateIndexBuilder { + b.options.Prefix = prefixes + return b +} + +// Filter sets FILTER. +func (b *CreateIndexBuilder) Filter(filter string) *CreateIndexBuilder { + b.options.Filter = filter + return b +} + +// DefaultLanguage sets LANGUAGE. +func (b *CreateIndexBuilder) DefaultLanguage(lang string) *CreateIndexBuilder { + b.options.DefaultLanguage = lang + return b +} + +// LanguageField sets LANGUAGE_FIELD. +func (b *CreateIndexBuilder) LanguageField(field string) *CreateIndexBuilder { + b.options.LanguageField = field + return b +} + +// Score sets SCORE. +func (b *CreateIndexBuilder) Score(score float64) *CreateIndexBuilder { + b.options.Score = score + return b +} + +// ScoreField sets SCORE_FIELD. +func (b *CreateIndexBuilder) ScoreField(field string) *CreateIndexBuilder { + b.options.ScoreField = field + return b +} + +// PayloadField sets PAYLOAD_FIELD. +func (b *CreateIndexBuilder) PayloadField(field string) *CreateIndexBuilder { + b.options.PayloadField = field + return b +} + +// NoOffsets includes NOOFFSETS. +func (b *CreateIndexBuilder) NoOffsets() *CreateIndexBuilder { b.options.NoOffsets = true; return b } + +// Temporary sets TEMPORARY seconds. +func (b *CreateIndexBuilder) Temporary(sec int) *CreateIndexBuilder { + b.options.Temporary = sec + return b +} + +// NoHL includes NOHL. +func (b *CreateIndexBuilder) NoHL() *CreateIndexBuilder { b.options.NoHL = true; return b } + +// NoFields includes NOFIELDS. +func (b *CreateIndexBuilder) NoFields() *CreateIndexBuilder { b.options.NoFields = true; return b } + +// NoFreqs includes NOFREQS. +func (b *CreateIndexBuilder) NoFreqs() *CreateIndexBuilder { b.options.NoFreqs = true; return b } + +// StopWords sets STOPWORDS. +func (b *CreateIndexBuilder) StopWords(words ...interface{}) *CreateIndexBuilder { + b.options.StopWords = words + return b +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *CreateIndexBuilder) SkipInitialScan() *CreateIndexBuilder { + b.options.SkipInitialScan = true + return b +} + +// Schema adds a FieldSchema. +func (b *CreateIndexBuilder) Schema(field *FieldSchema) *CreateIndexBuilder { + b.schema = append(b.schema, field) + return b +} + +// Run executes FT.CREATE and returns the status. +func (b *CreateIndexBuilder) Run() (string, error) { + cmd := b.c.FTCreate(b.ctx, b.index, b.options, b.schema...) + return cmd.Result() +} + +// ---------------------- +// DropIndexBuilder for FT.DROPINDEX +// ---------------------- +// DropIndexBuilder is a builder for FT.DROPINDEX +// EXPERIMENTAL: this API is subject to change, use with caution. +type DropIndexBuilder struct { + c *Client + ctx context.Context + index string + options *FTDropIndexOptions +} + +// NewDropIndexBuilder creates a new DropIndexBuilder for FT.DROPINDEX commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewDropIndexBuilder(ctx context.Context, index string) *DropIndexBuilder { + return &DropIndexBuilder{c: c, ctx: ctx, index: index} +} + +// DeleteRuncs includes DD. +func (b *DropIndexBuilder) DeleteDocs() *DropIndexBuilder { b.options.DeleteDocs = true; return b } + +// Run executes FT.DROPINDEX. +func (b *DropIndexBuilder) Run() (string, error) { + cmd := b.c.FTDropIndexWithArgs(b.ctx, b.index, b.options) + return cmd.Result() +} + +// ---------------------- +// AliasBuilder for FT.ALIAS* commands +// ---------------------- +// AliasBuilder is builder for FT.ALIAS* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type AliasBuilder struct { + c *Client + ctx context.Context + alias string + index string + action string // add|del|update +} + +// NewAliasBuilder creates a new AliasBuilder for FT.ALIAS* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewAliasBuilder(ctx context.Context, alias string) *AliasBuilder { + return &AliasBuilder{c: c, ctx: ctx, alias: alias} +} + +// Action sets the action for the alias builder. +func (b *AliasBuilder) Action(action string) *AliasBuilder { + b.action = action + return b +} + +// Add sets the action to "add" and requires an index. +func (b *AliasBuilder) Add(index string) *AliasBuilder { + b.action = "add" + b.index = index + return b +} + +// Del sets the action to "del". +func (b *AliasBuilder) Del() *AliasBuilder { + b.action = "del" + return b +} + +// Update sets the action to "update" and requires an index. +func (b *AliasBuilder) Update(index string) *AliasBuilder { + b.action = "update" + b.index = index + return b +} + +// Run executes the configured alias command. +func (b *AliasBuilder) Run() (string, error) { + switch b.action { + case "add": + cmd := b.c.FTAliasAdd(b.ctx, b.index, b.alias) + return cmd.Result() + case "del": + cmd := b.c.FTAliasDel(b.ctx, b.alias) + return cmd.Result() + case "update": + cmd := b.c.FTAliasUpdate(b.ctx, b.index, b.alias) + return cmd.Result() + } + return "", nil +} + +// ---------------------- +// ExplainBuilder for FT.EXPLAIN +// ---------------------- +// ExplainBuilder is builder for FT.EXPLAIN +// EXPERIMENTAL: this API is subject to change, use with caution. +type ExplainBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTExplainOptions +} + +// NewExplainBuilder creates a new ExplainBuilder for FT.EXPLAIN commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewExplainBuilder(ctx context.Context, index, query string) *ExplainBuilder { + return &ExplainBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTExplainOptions{}} +} + +// Dialect sets dialect for EXPLAINCLI. +func (b *ExplainBuilder) Dialect(d string) *ExplainBuilder { b.options.Dialect = d; return b } + +// Run executes FT.EXPLAIN and returns the plan. +func (b *ExplainBuilder) Run() (string, error) { + cmd := b.c.FTExplainWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// InfoBuilder for FT.INFO +// ---------------------- + +type FTInfoBuilder struct { + c *Client + ctx context.Context + index string +} + +// NewSearchInfoBuilder creates a new FTInfoBuilder for FT.INFO commands. +func (c *Client) NewSearchInfoBuilder(ctx context.Context, index string) *FTInfoBuilder { + return &FTInfoBuilder{c: c, ctx: ctx, index: index} +} + +// Run executes FT.INFO and returns detailed info. +func (b *FTInfoBuilder) Run() (FTInfoResult, error) { + cmd := b.c.FTInfo(b.ctx, b.index) + return cmd.Result() +} + +// ---------------------- +// SpellCheckBuilder for FT.SPELLCHECK +// ---------------------- +// SpellCheckBuilder is builder for FT.SPELLCHECK +// EXPERIMENTAL: this API is subject to change, use with caution. +type SpellCheckBuilder struct { + c *Client + ctx context.Context + index string + query string + options *FTSpellCheckOptions +} + +// NewSpellCheckBuilder creates a new SpellCheckBuilder for FT.SPELLCHECK commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSpellCheckBuilder(ctx context.Context, index, query string) *SpellCheckBuilder { + return &SpellCheckBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTSpellCheckOptions{}} +} + +// Distance sets MAXDISTANCE. +func (b *SpellCheckBuilder) Distance(d int) *SpellCheckBuilder { b.options.Distance = d; return b } + +// Terms sets INCLUDE or EXCLUDE terms. +func (b *SpellCheckBuilder) Terms(include bool, dictionary string, terms ...interface{}) *SpellCheckBuilder { + if b.options.Terms == nil { + b.options.Terms = &FTSpellCheckTerms{} + } + if include { + b.options.Terms.Inclusion = "INCLUDE" + } else { + b.options.Terms.Inclusion = "EXCLUDE" + } + b.options.Terms.Dictionary = dictionary + b.options.Terms.Terms = terms + return b +} + +// Dialect sets dialect version. +func (b *SpellCheckBuilder) Dialect(d int) *SpellCheckBuilder { b.options.Dialect = d; return b } + +// Run executes FT.SPELLCHECK and returns suggestions. +func (b *SpellCheckBuilder) Run() ([]SpellCheckResult, error) { + cmd := b.c.FTSpellCheckWithArgs(b.ctx, b.index, b.query, b.options) + return cmd.Result() +} + +// ---------------------- +// DictBuilder for FT.DICT* commands +// ---------------------- +// DictBuilder is builder for FT.DICT* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type DictBuilder struct { + c *Client + ctx context.Context + dict string + terms []interface{} + action string // add|del|dump +} + +// NewDictBuilder creates a new DictBuilder for FT.DICT* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewDictBuilder(ctx context.Context, dict string) *DictBuilder { + return &DictBuilder{c: c, ctx: ctx, dict: dict} +} + +// Action sets the action for the dictionary builder. +func (b *DictBuilder) Action(action string) *DictBuilder { + b.action = action + return b +} + +// Add sets the action to "add" and requires terms. +func (b *DictBuilder) Add(terms ...interface{}) *DictBuilder { + b.action = "add" + b.terms = terms + return b +} + +// Del sets the action to "del" and requires terms. +func (b *DictBuilder) Del(terms ...interface{}) *DictBuilder { + b.action = "del" + b.terms = terms + return b +} + +// Dump sets the action to "dump". +func (b *DictBuilder) Dump() *DictBuilder { + b.action = "dump" + return b +} + +// Run executes the configured dictionary command. +func (b *DictBuilder) Run() (interface{}, error) { + switch b.action { + case "add": + cmd := b.c.FTDictAdd(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "del": + cmd := b.c.FTDictDel(b.ctx, b.dict, b.terms...) + return cmd.Result() + case "dump": + cmd := b.c.FTDictDump(b.ctx, b.dict) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// TagValsBuilder for FT.TAGVALS +// ---------------------- +// TagValsBuilder is builder for FT.TAGVALS +// EXPERIMENTAL: this API is subject to change, use with caution. +type TagValsBuilder struct { + c *Client + ctx context.Context + index string + field string +} + +// NewTagValsBuilder creates a new TagValsBuilder for FT.TAGVALS commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewTagValsBuilder(ctx context.Context, index, field string) *TagValsBuilder { + return &TagValsBuilder{c: c, ctx: ctx, index: index, field: field} +} + +// Run executes FT.TAGVALS and returns tag values. +func (b *TagValsBuilder) Run() ([]string, error) { + cmd := b.c.FTTagVals(b.ctx, b.index, b.field) + return cmd.Result() +} + +// ---------------------- +// CursorBuilder for FT.CURSOR* +// ---------------------- +// CursorBuilder is builder for FT.CURSOR* commands +// EXPERIMENTAL: this API is subject to change, use with caution. +type CursorBuilder struct { + c *Client + ctx context.Context + index string + cursorId int64 + count int + action string // read|del +} + +// NewCursorBuilder creates a new CursorBuilder for FT.CURSOR* commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewCursorBuilder(ctx context.Context, index string, cursorId int64) *CursorBuilder { + return &CursorBuilder{c: c, ctx: ctx, index: index, cursorId: cursorId} +} + +// Action sets the action for the cursor builder. +func (b *CursorBuilder) Action(action string) *CursorBuilder { + b.action = action + return b +} + +// Read sets the action to "read". +func (b *CursorBuilder) Read() *CursorBuilder { + b.action = "read" + return b +} + +// Del sets the action to "del". +func (b *CursorBuilder) Del() *CursorBuilder { + b.action = "del" + return b +} + +// Count for READ. +func (b *CursorBuilder) Count(count int) *CursorBuilder { b.count = count; return b } + +// Run executes the cursor command. +func (b *CursorBuilder) Run() (interface{}, error) { + switch b.action { + case "read": + cmd := b.c.FTCursorRead(b.ctx, b.index, int(b.cursorId), b.count) + return cmd.Result() + case "del": + cmd := b.c.FTCursorDel(b.ctx, b.index, int(b.cursorId)) + return cmd.Result() + } + return nil, nil +} + +// ---------------------- +// SynUpdateBuilder for FT.SYNUPDATE +// ---------------------- +// SyncUpdateBuilder is builder for FT.SYNCUPDATE +// EXPERIMENTAL: this API is subject to change, use with caution. +type SynUpdateBuilder struct { + c *Client + ctx context.Context + index string + groupId interface{} + options *FTSynUpdateOptions + terms []interface{} +} + +// NewSynUpdateBuilder creates a new SynUpdateBuilder for FT.SYNUPDATE commands. +// EXPERIMENTAL: this API is subject to change, use with caution. +func (c *Client) NewSynUpdateBuilder(ctx context.Context, index string, groupId interface{}) *SynUpdateBuilder { + return &SynUpdateBuilder{c: c, ctx: ctx, index: index, groupId: groupId, options: &FTSynUpdateOptions{}} +} + +// SkipInitialScan includes SKIPINITIALSCAN. +func (b *SynUpdateBuilder) SkipInitialScan() *SynUpdateBuilder { + b.options.SkipInitialScan = true + return b +} + +// Terms adds synonyms to the group. +func (b *SynUpdateBuilder) Terms(terms ...interface{}) *SynUpdateBuilder { b.terms = terms; return b } + +// Run executes FT.SYNUPDATE. +func (b *SynUpdateBuilder) Run() (string, error) { + cmd := b.c.FTSynUpdateWithArgs(b.ctx, b.index, b.groupId, b.options, b.terms) + return cmd.Result() +} diff --git a/search_builders_test.go b/search_builders_test.go new file mode 100644 index 0000000000..bd8b6ff7c4 --- /dev/null +++ b/search_builders_test.go @@ -0,0 +1,680 @@ +package redis_test + +import ( + "context" + "fmt" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +var _ = Describe("RediSearch Builders", Label("search", "builders"), func() { + ctx := context.Background() + var client *redis.Client + + BeforeEach(func() { + client = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + expectCloseErr := client.Close() + Expect(expectCloseErr).NotTo(HaveOccurred()) + }) + + It("should create index and search with scores using builders", Label("search", "ftcreate", "ftsearch"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx1"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "foo", "hello world") + client.HSet(ctx, "doc2", "foo", "hello redis") + + res, err := client.NewSearchBuilder(ctx, "idx1", "hello").WithScores().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(2)) + for _, doc := range res.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + } + }) + + It("should aggregate using builders", Label("search", "ftaggregate"), func() { + _, err := client.NewCreateIndexBuilder(ctx, "idx2"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "n", FieldType: redis.SearchFieldTypeNumeric}). + Run() + Expect(err).NotTo(HaveOccurred()) + WaitForIndexing(client, "idx2") + + client.HSet(ctx, "d1", "n", 1) + client.HSet(ctx, "d2", "n", 2) + + agg, err := client.NewAggregateBuilder(ctx, "idx2", "*"). + GroupBy("@n"). + ReduceAs(redis.SearchCount, "count"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(len(agg.Rows)).To(Equal(2)) + }) + + It("should drop index using builder", Label("search", "ftdropindex"), func() { + Expect(client.NewCreateIndexBuilder(ctx, "idx3"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "x", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx3") + + dropVal, err := client.NewDropIndexBuilder(ctx, "idx3").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(dropVal).To(Equal("OK")) + }) + + It("should manage aliases using builder", Label("search", "ftalias"), func() { + Expect(client.NewCreateIndexBuilder(ctx, "idx4"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "t", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx4") + + addVal, err := client.NewAliasBuilder(ctx, "alias1").Add("idx4").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(addVal).To(Equal("OK")) + + _, err = client.NewSearchBuilder(ctx, "alias1", "*").Run() + Expect(err).NotTo(HaveOccurred()) + + delVal, err := client.NewAliasBuilder(ctx, "alias1").Del().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(delVal).To(Equal("OK")) + }) + + It("should explain query using ExplainBuilder", Label("search", "builders", "ftexplain"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_explain"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_explain") + + expl, err := client.NewExplainBuilder(ctx, "idx_explain", "foo").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(expl).To(ContainSubstring("UNION")) + }) + + It("should retrieve info using SearchInfo builder", Label("search", "builders", "ftinfo"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_info"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_info") + + i, err := client.NewSearchInfoBuilder(ctx, "idx_info").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(i.IndexName).To(Equal("idx_info")) + }) + + It("should spellcheck using builder", Label("search", "builders", "ftspellcheck"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_spell"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_spell") + + client.HSet(ctx, "doc1", "foo", "bar") + + _, err = client.NewSpellCheckBuilder(ctx, "idx_spell", "ba").Distance(1).Run() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should manage dictionary using DictBuilder", Label("search", "ftdict"), func() { + addCount, err := client.NewDictBuilder(ctx, "dict1").Add("a", "b").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(addCount).To(Equal(int64(2))) + + dump, err := client.NewDictBuilder(ctx, "dict1").Dump().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(dump).To(ContainElements("a", "b")) + + delCount, err := client.NewDictBuilder(ctx, "dict1").Del("a").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(delCount).To(Equal(int64(1))) + }) + + It("should tag values using TagValsBuilder", Label("search", "builders", "fttagvals"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_tag"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_tag") + + client.HSet(ctx, "doc1", "tags", "red,blue") + client.HSet(ctx, "doc2", "tags", "green,blue") + + vals, err := client.NewTagValsBuilder(ctx, "idx_tag", "tags").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(vals).To(BeAssignableToTypeOf([]string{})) + }) + + It("should cursor read and delete using CursorBuilder", Label("search", "builders", "ftcursor"), func() { + Expect(client.NewCreateIndexBuilder(ctx, "idx5"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "f", FieldType: redis.SearchFieldTypeText}). + Run()).To(Equal("OK")) + WaitForIndexing(client, "idx5") + client.HSet(ctx, "doc1", "f", "hello") + client.HSet(ctx, "doc2", "f", "world") + + cursorBuilder := client.NewCursorBuilder(ctx, "idx5", 1) + Expect(cursorBuilder).NotTo(BeNil()) + + cursorBuilder = cursorBuilder.Count(10) + Expect(cursorBuilder).NotTo(BeNil()) + + delBuilder := client.NewCursorBuilder(ctx, "idx5", 1) + Expect(delBuilder).NotTo(BeNil()) + }) + + It("should update synonyms using SynUpdateBuilder", Label("search", "builders", "ftsynupdate"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_syn"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_syn") + + syn, err := client.NewSynUpdateBuilder(ctx, "idx_syn", "grp1").Terms("a", "b").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(syn).To(Equal("OK")) + }) + + It("should test SearchBuilder with NoContent and Verbatim", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nocontent"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Weight: 5}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_nocontent") + + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") + + res, err := client.NewSearchBuilder(ctx, "idx_nocontent", "search engine"). + NoContent(). + Verbatim(). + Limit(0, 5). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("doc1")) + // NoContent means no fields should be returned + Expect(res.Docs[0].Fields).To(BeEmpty()) + }) + + It("should test SearchBuilder with NoStopWords", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_nostop"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_nostop") + + client.HSet(ctx, "doc1", "txt", "hello world") + client.HSet(ctx, "doc2", "txt", "test document") + + // Test that NoStopWords method can be called and search works + res, err := client.NewSearchBuilder(ctx, "idx_nostop", "hello").NoContent().NoStopWords().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + }) + + It("should test SearchBuilder with filters", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_filters"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}). + Schema(&redis.FieldSchema{FieldName: "loc", FieldType: redis.SearchFieldTypeGeo}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_filters") + + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 3.141, "loc", "-0.441,51.458") + client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2, "loc", "-0.1,51.2") + + // Test numeric filter + res1, err := client.NewSearchBuilder(ctx, "idx_filters", "foo"). + Filter("num", 2, 4). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + + // Test geo filter + res2, err := client.NewSearchBuilder(ctx, "idx_filters", "foo"). + GeoFilter("loc", -0.44, 51.45, 10, "km"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + }) + + It("should test SearchBuilder with sorting", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_sort"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_sort") + + client.HSet(ctx, "doc1", "txt", "foo bar", "num", 1) + client.HSet(ctx, "doc2", "txt", "foo baz", "num", 2) + client.HSet(ctx, "doc3", "txt", "foo qux", "num", 3) + + // Test ascending sort + res1, err := client.NewSearchBuilder(ctx, "idx_sort", "foo"). + SortBy("num", true). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(3)) + Expect(res1.Docs[0].ID).To(Equal("doc1")) + Expect(res1.Docs[1].ID).To(Equal("doc2")) + Expect(res1.Docs[2].ID).To(Equal("doc3")) + + // Test descending sort + res2, err := client.NewSearchBuilder(ctx, "idx_sort", "foo"). + SortBy("num", false). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(3)) + Expect(res2.Docs[0].ID).To(Equal("doc3")) + Expect(res2.Docs[1].ID).To(Equal("doc2")) + Expect(res2.Docs[2].ID).To(Equal("doc1")) + }) + + It("should test SearchBuilder with InKeys and InFields", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_in"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_in") + + client.HSet(ctx, "doc1", "title", "hello world", "body", "lorem ipsum") + client.HSet(ctx, "doc2", "title", "foo bar", "body", "hello world") + client.HSet(ctx, "doc3", "title", "baz qux", "body", "dolor sit") + + // Test InKeys + res1, err := client.NewSearchBuilder(ctx, "idx_in", "hello"). + InKeys("doc1", "doc2"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + + // Test InFields + res2, err := client.NewSearchBuilder(ctx, "idx_in", "hello"). + InFields("title"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].ID).To(Equal("doc1")) + }) + + It("should test SearchBuilder with Return fields", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_return"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "body", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_return") + + client.HSet(ctx, "doc1", "title", "hello", "body", "world", "num", 42) + + // Test ReturnFields + res1, err := client.NewSearchBuilder(ctx, "idx_return", "hello"). + ReturnFields("title", "num"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + Expect(res1.Docs[0].Fields).To(HaveKey("title")) + Expect(res1.Docs[0].Fields).To(HaveKey("num")) + Expect(res1.Docs[0].Fields).NotTo(HaveKey("body")) + + // Test ReturnAs + res2, err := client.NewSearchBuilder(ctx, "idx_return", "hello"). + ReturnAs("title", "doc_title"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].Fields).To(HaveKey("doc_title")) + Expect(res2.Docs[0].Fields).NotTo(HaveKey("title")) + }) + + It("should test SearchBuilder with advanced options", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_advanced"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_advanced") + + client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") + client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank") + + // Test with scores and different scorers + res1, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). + WithScores(). + Scorer("TFIDF"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(2)) + for _, doc := range res1.Docs { + Expect(*doc.Score).To(BeNumerically(">", 0)) + } + + res2, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). + WithScores(). + Payload("test_payload"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(2)) + + // Test with Slop and InOrder + res3, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick brown"). + Slop(1). + InOrder(). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(Equal(1)) + + // Test with Language and Expander + res4, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). + Language("english"). + Expander("SYNONYM"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeNumerically(">=", 0)) + + // Test with Timeout + res5, err := client.NewSearchBuilder(ctx, "idx_advanced", "quick"). + Timeout(1000). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res5.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with Params and Dialect", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_params"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_params") + + client.HSet(ctx, "doc1", "name", "Alice") + client.HSet(ctx, "doc2", "name", "Bob") + client.HSet(ctx, "doc3", "name", "Carol") + + // Test with single param + res1, err := client.NewSearchBuilder(ctx, "idx_params", "@name:$name"). + Param("name", "Alice"). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + Expect(res1.Docs[0].ID).To(Equal("doc1")) + + // Test with multiple params using ParamsMap + params := map[string]interface{}{ + "name1": "Bob", + "name2": "Carol", + } + res2, err := client.NewSearchBuilder(ctx, "idx_params", "@name:($name1|$name2)"). + ParamsMap(params). + Dialect(2). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with Limit and CountOnly", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_limit"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_limit") + + for i := 1; i <= 10; i++ { + client.HSet(ctx, fmt.Sprintf("doc%d", i), "txt", "test document") + } + + // Test with Limit + res1, err := client.NewSearchBuilder(ctx, "idx_limit", "test"). + Limit(2, 3). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(10)) + Expect(len(res1.Docs)).To(Equal(3)) + + // Test with CountOnly + res2, err := client.NewSearchBuilder(ctx, "idx_limit", "test"). + CountOnly(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(10)) + Expect(len(res2.Docs)).To(Equal(0)) + }) + + It("should test SearchBuilder with WithSortByCount and SortBy", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_payloads"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "num", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_payloads") + + client.HSet(ctx, "doc1", "txt", "hello", "num", 1) + client.HSet(ctx, "doc2", "txt", "world", "num", 2) + + // Test WithSortByCount and SortBy + res, err := client.NewSearchBuilder(ctx, "idx_payloads", "*"). + SortBy("num", true). + WithSortByCount(). + NoContent(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(2)) + }) + + It("should test SearchBuilder with JSON", Label("search", "ftsearch", "builders", "json"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_json"). + OnJSON(). + Prefix("king:"). + Schema(&redis.FieldSchema{FieldName: "$.name", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_json") + + client.JSONSet(ctx, "king:1", "$", `{"name": "henry"}`) + client.JSONSet(ctx, "king:2", "$", `{"name": "james"}`) + + res, err := client.NewSearchBuilder(ctx, "idx_json", "henry").Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("king:1")) + Expect(res.Docs[0].Fields["$"]).To(Equal(`{"name":"henry"}`)) + }) + + It("should test SearchBuilder with vector search", Label("search", "ftsearch", "builders", "vector"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_vector"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_vector") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + res, err := client.NewSearchBuilder(ctx, "idx_vector", "*=>[KNN 2 @v $vec]"). + ReturnFields("__v_score"). + SortBy("__v_score", true). + Dialect(2). + Param("vec", "aaaaaaaa"). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(Equal("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(Equal("0")) + }) + + It("should test SearchBuilder with complex filtering and aggregation", Label("search", "ftsearch", "builders"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_complex"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "category", FieldType: redis.SearchFieldTypeTag}). + Schema(&redis.FieldSchema{FieldName: "price", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Schema(&redis.FieldSchema{FieldName: "location", FieldType: redis.SearchFieldTypeGeo}). + Schema(&redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_complex") + + client.HSet(ctx, "product1", "category", "electronics", "price", 100, "location", "-0.1,51.5", "description", "smartphone device") + client.HSet(ctx, "product2", "category", "electronics", "price", 200, "location", "-0.2,51.6", "description", "laptop computer") + client.HSet(ctx, "product3", "category", "books", "price", 20, "location", "-0.3,51.7", "description", "programming guide") + + res, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{electronics} @description:(device|computer)"). + Filter("price", 50, 250). + GeoFilter("location", -0.15, 51.55, 50, "km"). + SortBy("price", true). + ReturnFields("category", "price", "description"). + Limit(0, 10). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(BeNumerically(">=", 1)) + + res2, err := client.NewSearchBuilder(ctx, "idx_complex", "@category:{$cat} @price:[$min $max]"). + ParamsMap(map[string]interface{}{ + "cat": "electronics", + "min": 150, + "max": 300, + }). + Dialect(2). + WithScores(). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(1)) + Expect(res2.Docs[0].ID).To(Equal("product2")) + }) + + It("should test SearchBuilder error handling and edge cases", Label("search", "ftsearch", "builders", "edge-cases"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_edge"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_edge") + + client.HSet(ctx, "doc1", "txt", "hello world") + + // Test empty query + res1, err := client.NewSearchBuilder(ctx, "idx_edge", "*").NoContent().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res1.Total).To(Equal(1)) + + // Test query with no results + res2, err := client.NewSearchBuilder(ctx, "idx_edge", "nonexistent").NoContent().Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res2.Total).To(Equal(0)) + + // Test with multiple chained methods + res3, err := client.NewSearchBuilder(ctx, "idx_edge", "hello"). + WithScores(). + NoContent(). + Verbatim(). + InOrder(). + Slop(0). + Timeout(5000). + Language("english"). + Dialect(2). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(Equal(1)) + }) + + It("should test SearchBuilder method chaining", Label("search", "ftsearch", "builders", "fluent"), func() { + createVal, err := client.NewCreateIndexBuilder(ctx, "idx_fluent"). + OnHash(). + Schema(&redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText}). + Schema(&redis.FieldSchema{FieldName: "tags", FieldType: redis.SearchFieldTypeTag}). + Schema(&redis.FieldSchema{FieldName: "score", FieldType: redis.SearchFieldTypeNumeric, Sortable: true}). + Run() + Expect(err).NotTo(HaveOccurred()) + Expect(createVal).To(Equal("OK")) + WaitForIndexing(client, "idx_fluent") + + client.HSet(ctx, "doc1", "title", "Redis Search Tutorial", "tags", "redis,search,tutorial", "score", 95) + client.HSet(ctx, "doc2", "title", "Advanced Redis", "tags", "redis,advanced", "score", 88) + + builder := client.NewSearchBuilder(ctx, "idx_fluent", "@title:(redis) @tags:{search}") + result := builder. + WithScores(). + Filter("score", 90, 100). + SortBy("score", false). + ReturnFields("title", "score"). + Limit(0, 5). + Dialect(2). + Timeout(1000). + Language("english") + + res, err := result.Run() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Total).To(Equal(1)) + Expect(res.Docs[0].ID).To(Equal("doc1")) + Expect(res.Docs[0].Fields["title"]).To(Equal("Redis Search Tutorial")) + Expect(*res.Docs[0].Score).To(BeNumerically(">", 0)) + }) +}) From 6f1aac4aa3272776f85aae549743d8483ebf4630 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:00:37 +0300 Subject: [PATCH 223/230] chore(release): 9.12.0-beta.1 (#3460) --- CHANGELOG.md | 133 -------------------- RELEASE-NOTES.md | 185 ++++++++++++++++++++++++++++ example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 +- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 +- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 14 files changed, 201 insertions(+), 149 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e1652b179a..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,133 +0,0 @@ -## Unreleased - -### Changed - -* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) - Users can use the OpenTelemetry sampler to control the sampling behavior. - For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep - a similar behavior (drop orphan spans) of `go-redis` as before. - -## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) - - -### Features - -* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602)) -* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe)) -* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af)) - - - -## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01) - - -### Bug Fixes - -* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241)) - - -### Features - -* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e)) -* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8)) -* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af)) - - - -## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02) - -### New Features - -- feat(scan): scan time.Time sets the default decoding (#2413) -- Add support for CLUSTER LINKS command (#2504) -- Add support for acl dryrun command (#2502) -- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500) -- Add support for LCS Command (#2480) -- Add support for BZMPOP (#2456) -- Adding support for ZMPOP command (#2408) -- Add support for LMPOP (#2440) -- feat: remove pool unused fields (#2438) -- Expiretime and PExpireTime (#2426) -- Implement `FUNCTION` group of commands (#2475) -- feat(zadd): add ZAddLT and ZAddGT (#2429) -- Add: Support for COMMAND LIST command (#2491) -- Add support for BLMPOP (#2442) -- feat: check pipeline.Do to prevent confusion with Exec (#2517) -- Function stats, function kill, fcall and fcall_ro (#2486) -- feat: Add support for CLUSTER SHARDS command (#2507) -- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498) - -### Fixed - -- fix: eval api cmd.SetFirstKeyPos (#2501) -- fix: limit the number of connections created (#2441) -- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479) -- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458) -- fix: group lag can be null (#2448) - -### Maintenance - -- Updating to the latest version of redis (#2508) -- Allowing for running tests on a port other than the fixed 6380 (#2466) -- redis 7.0.8 in tests (#2450) -- docs: Update redisotel example for v9 (#2425) -- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476) -- chore: add Chinese translation (#2436) -- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421) -- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420) -- chore(deps): bump actions/setup-go from 3 to 4 (#2495) -- docs: add instructions for the HSet api (#2503) -- docs: add reading lag field comment (#2451) -- test: update go mod before testing(go mod tidy) (#2423) -- docs: fix comment typo (#2505) -- test: remove testify (#2463) -- refactor: change ListElementCmd to KeyValuesCmd. (#2443) -- fix(appendArg): appendArg case special type (#2489) - -## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01) - -### Features - -* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65)) - -## v9 2023-01-30 - -### Breaking - -- Changed Pipelines to not be thread-safe any more. - -### Added - -- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was - contributed by @monkey92t who has done the majority of work in this release. -- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts - and deadlines. See - [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details. -- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example, - `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`. -- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html) -- Added `redis.HasErrorPrefix` to help working with errors. - -### Changed - -- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is - completely gone in v9. -- Reworked hook interface and added `DialHook`. -- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See - [example](example/otel) and - [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html). -- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making - an allocation. -- Renamed the option `MaxConnAge` to `ConnMaxLifetime`. -- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`. -- Removed connection reaper in favor of `MaxIdleConns`. -- Removed `WithContext` since `context.Context` can be passed directly as an arg. -- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and - it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to - reset commands for some reason. - -### Fixed - -- Improved and fixed pipeline retries. -- As usually, added support for more commands and fixed some bugs. diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 64754902da..1fb51275ae 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,54 @@ # Release Notes +# 9.12.0-beta.1 (2025-08-04) + +## 🚀 Highlights + +- This is a beta release for Redis 8.2 support. +- Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands. +- Adds support for `EPSILON` option in `FT.VSIM`. +- Includes bug fixes and improvements related to search and community contributions for [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel). + +## Changes + +- chore(github): Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458)) +- chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459)) +- Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450)) +- feat: Add "skip_verify" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428)) +- fix: `errors.Join` requires Go 1.20 or later ([#3442](https://github.com/redis/go-redis/pull/3442)) +- DOC-4344 document quickstart examples ([#3426](https://github.com/redis/go-redis/pull/3426)) +- feat(bitop): add support for the new bitop operations ([#3409](https://github.com/redis/go-redis/pull/3409)) + +## 🚀 New Features + +- Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436)) +- Add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453)) +- Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449)) +- VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454)) +- Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444)) + +## 🐛 Bug Fixes + +- fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457)) +- [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403)) + +## 🧰 Maintenance + +- DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443)) +- Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461)) +- bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451)) + +## Contributors +We'd like to thank all the contributors who worked on this release! + +[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn) and [@smnvdev](https://github.com/smnvdev) + +## New Contributors +* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in https://github.com/redis/go-redis/pull/3409 +* [@smnvdev](https://github.com/smnvdev) made their first contribution in https://github.com/redis/go-redis/pull/3403 +* [@rokn](https://github.com/rokn) made their first contribution in https://github.com/redis/go-redis/pull/3444 + + # 9.11.0 (2025-06-24) ## 🚀 Highlights @@ -199,3 +248,139 @@ For a complete list of changes, see the [full changelog](https://github.com/redi We would like to thank all the contributors who made this release possible: [@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi) + + +# Old Changelog +## Unreleased + +### Changed + +* `go-redis` won't skip span creation if the parent spans is not recording. ([#2980](https://github.com/redis/go-redis/issues/2980)) + Users can use the OpenTelemetry sampler to control the sampling behavior. + For instance, you can use the `ParentBased(NeverSample())` sampler from `go.opentelemetry.io/otel/sdk/trace` to keep + a similar behavior (drop orphan spans) of `go-redis` as before. + +## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29) + + +### Features + +* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602)) +* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe)) +* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af)) + + + +## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01) + + +### Bug Fixes + +* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241)) + + +### Features + +* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e)) +* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8)) +* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af)) + + + +## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02) + +### New Features + +- feat(scan): scan time.Time sets the default decoding (#2413) +- Add support for CLUSTER LINKS command (#2504) +- Add support for acl dryrun command (#2502) +- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500) +- Add support for LCS Command (#2480) +- Add support for BZMPOP (#2456) +- Adding support for ZMPOP command (#2408) +- Add support for LMPOP (#2440) +- feat: remove pool unused fields (#2438) +- Expiretime and PExpireTime (#2426) +- Implement `FUNCTION` group of commands (#2475) +- feat(zadd): add ZAddLT and ZAddGT (#2429) +- Add: Support for COMMAND LIST command (#2491) +- Add support for BLMPOP (#2442) +- feat: check pipeline.Do to prevent confusion with Exec (#2517) +- Function stats, function kill, fcall and fcall_ro (#2486) +- feat: Add support for CLUSTER SHARDS command (#2507) +- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498) + +### Fixed + +- fix: eval api cmd.SetFirstKeyPos (#2501) +- fix: limit the number of connections created (#2441) +- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479) +- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458) +- fix: group lag can be null (#2448) + +### Maintenance + +- Updating to the latest version of redis (#2508) +- Allowing for running tests on a port other than the fixed 6380 (#2466) +- redis 7.0.8 in tests (#2450) +- docs: Update redisotel example for v9 (#2425) +- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476) +- chore: add Chinese translation (#2436) +- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421) +- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420) +- chore(deps): bump actions/setup-go from 3 to 4 (#2495) +- docs: add instructions for the HSet api (#2503) +- docs: add reading lag field comment (#2451) +- test: update go mod before testing(go mod tidy) (#2423) +- docs: fix comment typo (#2505) +- test: remove testify (#2463) +- refactor: change ListElementCmd to KeyValuesCmd. (#2443) +- fix(appendArg): appendArg case special type (#2489) + +## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01) + +### Features + +* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65)) + +## v9 2023-01-30 + +### Breaking + +- Changed Pipelines to not be thread-safe any more. + +### Added + +- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was + contributed by @monkey92t who has done the majority of work in this release. +- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts + and deadlines. See + [Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details. +- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example, + `redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`. +- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See + [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html) +- Added `redis.HasErrorPrefix` to help working with errors. + +### Changed + +- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is + completely gone in v9. +- Reworked hook interface and added `DialHook`. +- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See + [example](example/otel) and + [documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html). +- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making + an allocation. +- Renamed the option `MaxConnAge` to `ConnMaxLifetime`. +- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`. +- Removed connection reaper in favor of `MaxIdleConns`. +- Removed `WithContext` since `context.Context` can be passed directly as an arg. +- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and + it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to + reset commands for some reason. + +### Fixed + +- Improved and fixed pipeline retries. +- As usually, added support for more commands and fixed some bugs. \ No newline at end of file diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 08144430f3..fb2fc1cb4b 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.12.0-beta.1 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 19611d46c5..d2f59e10c4 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.11.0 +require github.com/redis/go-redis/v9 v9.12.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index 89293593d4..af085d3477 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.12.0-beta.1 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 1706c42e9a..71bced330d 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.11.0 +require github.com/redis/go-redis/v9 v9.12.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 26653fbc1e..ac33ff812c 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.11.0 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/extra/redisotel/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0-beta.1 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 6eb04204ad..c0542a24d4 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.11.0 +require github.com/redis/go-redis/v9 v9.12.0-beta.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 89293593d4..af085d3477 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.12.0-beta.1 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 5e01aba612..dfe2a8d2ee 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0-beta.1 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index c8e8f3c2fb..2760c09a71 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.12.0-beta.1 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index b3c2db5fde..644fa6808a 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.11.0 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0-beta.1 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 74613deb36..b4e9304fa2 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.11.0 + github.com/redis/go-redis/v9 v9.12.0-beta.1 ) require ( diff --git a/version.go b/version.go index e6dbfd14e3..0b213b0657 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.11.0" + return "9.12.0-beta.1" } From bbadd655edbdec22122aa930fe5b4c0ddb1c0a27 Mon Sep 17 00:00:00 2001 From: cxljs Date: Mon, 4 Aug 2025 22:22:16 +0800 Subject: [PATCH 224/230] chore(doc): improve code readability (#3446) - replace two similar functions `appendUniqueNode` and `appendIfNotExists` with a generic function. - simplify the implementation of the `get` method in `clusterNodes` - keep the member name `_generation` of `clusterNodes` consistent with other types. - rename a data member `_masterAddr` to `masterAddr`. Signed-off-by: Xiaolong Chen --- osscluster.go | 60 ++++++++++++++++++--------------------------------- redis.go | 2 +- sentinel.go | 14 ++++++------ 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/osscluster.go b/osscluster.go index ad654821d1..83817ca374 100644 --- a/osscluster.go +++ b/osscluster.go @@ -364,8 +364,7 @@ type clusterNode struct { failing uint32 // atomic loaded uint32 // atomic - // last time the latency measurement was performed for the node, stored in nanoseconds - // from epoch + // last time the latency measurement was performed for the node, stored in nanoseconds from epoch lastLatencyMeasurement int64 // atomic } @@ -502,13 +501,12 @@ type clusterNodes struct { closed bool onNewNode []func(rdb *Client) - _generation uint32 // atomic + generation uint32 // atomic } func newClusterNodes(opt *ClusterOptions) *clusterNodes { return &clusterNodes{ - opt: opt, - + opt: opt, addrs: opt.Addrs, nodes: make(map[string]*clusterNode), } @@ -568,12 +566,11 @@ func (c *clusterNodes) Addrs() ([]string, error) { } func (c *clusterNodes) NextGeneration() uint32 { - return atomic.AddUint32(&c._generation, 1) + return atomic.AddUint32(&c.generation, 1) } // GC removes unused nodes. func (c *clusterNodes) GC(generation uint32) { - //nolint:prealloc var collected []*clusterNode c.mu.Lock() @@ -626,23 +623,20 @@ func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) { fn(node.Client) } - c.addrs = appendIfNotExists(c.addrs, addr) + c.addrs = appendIfNotExist(c.addrs, addr) c.nodes[addr] = node return node, nil } func (c *clusterNodes) get(addr string) (*clusterNode, error) { - var node *clusterNode - var err error c.mu.RLock() + defer c.mu.RUnlock() + if c.closed { - err = pool.ErrClosed - } else { - node = c.nodes[addr] + return nil, pool.ErrClosed } - c.mu.RUnlock() - return node, err + return c.nodes[addr], nil } func (c *clusterNodes) All() ([]*clusterNode, error) { @@ -673,8 +667,9 @@ func (c *clusterNodes) Random() (*clusterNode, error) { //------------------------------------------------------------------------------ type clusterSlot struct { - start, end int - nodes []*clusterNode + start int + end int + nodes []*clusterNode } type clusterSlotSlice []*clusterSlot @@ -734,9 +729,9 @@ func newClusterState( nodes = append(nodes, node) if i == 0 { - c.Masters = appendUniqueNode(c.Masters, node) + c.Masters = appendIfNotExist(c.Masters, node) } else { - c.Slaves = appendUniqueNode(c.Slaves, node) + c.Slaves = appendIfNotExist(c.Slaves, node) } } @@ -1295,7 +1290,7 @@ func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) { continue } - return newClusterState(c.nodes, slots, node.Client.opt.Addr) + return newClusterState(c.nodes, slots, addr) } /* @@ -2017,7 +2012,7 @@ func (c *ClusterClient) MasterForKey(ctx context.Context, key string) (*Client, if err != nil { return nil, err } - return node.Client, err + return node.Client, nil } func (c *ClusterClient) context(ctx context.Context) context.Context { @@ -2027,26 +2022,13 @@ func (c *ClusterClient) context(ctx context.Context) context.Context { return context.Background() } -func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode { - for _, n := range nodes { - if n == node { - return nodes - } - } - return append(nodes, node) -} - -func appendIfNotExists(ss []string, es ...string) []string { -loop: - for _, e := range es { - for _, s := range ss { - if s == e { - continue loop - } +func appendIfNotExist[T comparable](vals []T, newVal T) []T { + for _, v := range vals { + if v == newVal { + return vals } - ss = append(ss, e) } - return ss + return append(vals, newVal) } //------------------------------------------------------------------------------ diff --git a/redis.go b/redis.go index a368623aa0..43ab401f3d 100644 --- a/redis.go +++ b/redis.go @@ -383,7 +383,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { + if err = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); err == nil { // Authentication successful with HELLO command } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal diff --git a/sentinel.go b/sentinel.go index 83e4069d80..9c90d5b787 100644 --- a/sentinel.go +++ b/sentinel.go @@ -657,10 +657,10 @@ type sentinelFailover struct { onFailover func(ctx context.Context, addr string) onUpdate func(ctx context.Context) - mu sync.RWMutex - _masterAddr string - sentinel *SentinelClient - pubsub *PubSub + mu sync.RWMutex + masterAddr string + sentinel *SentinelClient + pubsub *PubSub } func (c *sentinelFailover) Close() error { @@ -921,7 +921,7 @@ func parseReplicaAddrs(addrs []map[string]string, keepDisconnected bool) []strin func (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) { c.mu.RLock() - currentAddr := c._masterAddr //nolint:ifshort + currentAddr := c.masterAddr //nolint:ifshort c.mu.RUnlock() if addr == currentAddr { @@ -931,10 +931,10 @@ func (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) { c.mu.Lock() defer c.mu.Unlock() - if addr == c._masterAddr { + if addr == c.masterAddr { return } - c._masterAddr = addr + c.masterAddr = addr internal.Logger.Printf(ctx, "sentinel: new master=%q addr=%q", c.opt.MasterName, addr) From d0717e5b841475fd9b30a2ea120eb2d76ee3e9ec Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Tue, 5 Aug 2025 12:49:28 +0100 Subject: [PATCH 225/230] chore(github): merges into one job with two steps (#3463) Signed-off-by: Elena Kolevska --- .github/workflows/stale-issues.yml | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index f24d4f9321..38737b7259 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -18,10 +18,12 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + # First step: Handle regular issues (excluding needs-information) + - name: Mark regular issues as stale + uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - dry-run: true + debug-only: true # Default stale policy days-before-stale: ${{ env.DAYS_BEFORE_STALE }} @@ -32,15 +34,15 @@ jobs: stale-pr-label: "stale" stale-issue-message: | - This issue has been automatically marked as stale due to inactivity. - It will be closed in 30 days if no further activity occurs. + This issue has been automatically marked as stale due to inactivity. + It will be closed in 30 days if no further activity occurs. If you believe this issue is still relevant, please add a comment to keep it open. close-issue-message: | - This issue has been automatically closed due to inactivity. + This issue has been automatically closed due to inactivity. If you believe this issue is still relevant, please reopen it or create a new issue with updated information. - # Exclude needs-information issues from this job + # Exclude needs-information issues from this step exempt-issue-labels: 'no-stale,needs-information' # Remove stale label when issue/PR becomes active again @@ -51,24 +53,22 @@ jobs: days-before-pr-close: ${{ env.DAYS_BEFORE_CLOSE }} stale-pr-message: | - This pull request has been automatically marked as stale due to inactivity. + This pull request has been automatically marked as stale due to inactivity. It will be closed in 30 days if no further activity occurs. close-pr-message: | - This pull request has been automatically closed due to inactivity. + This pull request has been automatically closed due to inactivity. If you would like to continue this work, please reopen the PR or create a new one. # Only exclude no-stale PRs (needs-information PRs follow standard timeline) exempt-pr-labels: 'no-stale' - # Separate job for needs-information issues ONLY with accelerated timeline - stale-needs-info: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v9 + # Second step: Handle needs-information issues with accelerated timeline + - name: Mark needs-information issues as stale + uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - dry-run: true + debug-only: true # Accelerated timeline for needs-information days-before-stale: ${{ env.NEEDS_INFO_DAYS_BEFORE_STALE }} @@ -81,15 +81,15 @@ jobs: only-issue-labels: 'needs-information' stale-issue-message: | - This issue has been marked as stale because it requires additional information - that has not been provided for 30 days. It will be closed in 7 days if the + This issue has been marked as stale because it requires additional information + that has not been provided for 30 days. It will be closed in 7 days if the requested information is not provided. close-issue-message: | - This issue has been closed because the requested information was not provided within the specified timeframe. + This issue has been closed because the requested information was not provided within the specified timeframe. If you can provide the missing information, please reopen this issue or create a new one. - # Disable PR processing for this job + # Disable PR processing for this step days-before-pr-stale: -1 days-before-pr-close: -1 From 1aa1d845ef297f5683a8562a599656fa8d8c283b Mon Sep 17 00:00:00 2001 From: Mykhailo Alipa <6442572+strobil@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:00:33 +0200 Subject: [PATCH 226/230] feat(ring): specify custom health check func via HeartbeatFn option (#2940) * specify custom health check func via ShardHealthCheckFn option * ShardHealthCheckFn renamed to HeartbeatFn --------- Co-authored-by: Mykhailo Alipa Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- ring.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ring.go b/ring.go index 0d73e0101c..b0859b6419 100644 --- a/ring.go +++ b/ring.go @@ -24,6 +24,12 @@ import ( var errRingShardsDown = errors.New("redis: all ring shards are down") +// defaultHeartbeatFn is the default function used to check the shard liveness +var defaultHeartbeatFn = func(ctx context.Context, client *Client) bool { + err := client.Ping(ctx).Err() + return err == nil || err == pool.ErrPoolTimeout +} + //------------------------------------------------------------------------------ type ConsistentHash interface { @@ -56,10 +62,14 @@ type RingOptions struct { // ClientName will execute the `CLIENT SETNAME ClientName` command for each conn. ClientName string - // Frequency of PING commands sent to check shards availability. + // Frequency of executing HeartbeatFn to check shards availability. // Shard is considered down after 3 subsequent failed checks. HeartbeatFrequency time.Duration + // A function used to check the shard liveness + // if not set, defaults to defaultHeartbeatFn + HeartbeatFn func(ctx context.Context, client *Client) bool + // NewConsistentHash returns a consistent hash that is used // to distribute keys across the shards. // @@ -157,6 +167,10 @@ func (opt *RingOptions) init() { opt.HeartbeatFrequency = 500 * time.Millisecond } + if opt.HeartbeatFn == nil { + opt.HeartbeatFn = defaultHeartbeatFn + } + if opt.NewConsistentHash == nil { opt.NewConsistentHash = newRendezvous } @@ -474,8 +488,7 @@ func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) { // note: `c.List()` return a shadow copy of `[]*ringShard`. for _, shard := range c.List() { - err := shard.Client.Ping(ctx).Err() - isUp := err == nil || err == pool.ErrPoolTimeout + isUp := c.opt.HeartbeatFn(ctx, shard.Client) if shard.Vote(isUp) { internal.Logger.Printf(ctx, "ring shard state changed: %s", shard) rebalance = true From ef444ea2249170a8e3386f1ef10d1a843d0a7c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catt=C4=AB=20Cr=C5=ABd=C4=93l=C4=93s?= <17695588+wzy9607@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:15:34 +0800 Subject: [PATCH 227/230] fix(redisotel): fix buggy append in reportPoolStats (#3122) The current append twice to `conf.attrs` approach in `reportPoolStats` may result in unexpected idleAttrs, due to `append` [can mutate](https://github.com/golang/go/issues/29115#issuecomment-444669036) the underlying array of the original slice, as demonstrated at . Also, I replaced `metric.WithAttributes` in `reportPoolStats` with `metric.WithAttributeSet`, since `WithAttributes` is just `WithAttributeSet` with some extra works that are not needed here, see . Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- extra/redisotel/metrics.go | 27 ++++++++++------- extra/redisotel/metrics_test.go | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 extra/redisotel/metrics_test.go diff --git a/extra/redisotel/metrics.go b/extra/redisotel/metrics.go index d9a1c72196..fba5f3ac2e 100644 --- a/extra/redisotel/metrics.go +++ b/extra/redisotel/metrics.go @@ -113,10 +113,15 @@ func registerClient(rdb *redis.Client, conf *config, state *metricsState) error return nil } +func poolStatsAttrs(conf *config) (poolAttrs, idleAttrs, usedAttrs attribute.Set) { + poolAttrs = attribute.NewSet(conf.attrs...) + idleAttrs = attribute.NewSet(append(poolAttrs.ToSlice(), attribute.String("state", "idle"))...) + usedAttrs = attribute.NewSet(append(poolAttrs.ToSlice(), attribute.String("state", "used"))...) + return +} + func reportPoolStats(rdb *redis.Client, conf *config) (metric.Registration, error) { - labels := conf.attrs - idleAttrs := append(labels, attribute.String("state", "idle")) - usedAttrs := append(labels, attribute.String("state", "used")) + poolAttrs, idleAttrs, usedAttrs := poolStatsAttrs(conf) idleMax, err := conf.meter.Int64ObservableUpDownCounter( "db.client.connections.idle.max", @@ -179,16 +184,16 @@ func reportPoolStats(rdb *redis.Client, conf *config) (metric.Registration, erro func(ctx context.Context, o metric.Observer) error { stats := rdb.PoolStats() - o.ObserveInt64(idleMax, int64(redisConf.MaxIdleConns), metric.WithAttributes(labels...)) - o.ObserveInt64(idleMin, int64(redisConf.MinIdleConns), metric.WithAttributes(labels...)) - o.ObserveInt64(connsMax, int64(redisConf.PoolSize), metric.WithAttributes(labels...)) + o.ObserveInt64(idleMax, int64(redisConf.MaxIdleConns), metric.WithAttributeSet(poolAttrs)) + o.ObserveInt64(idleMin, int64(redisConf.MinIdleConns), metric.WithAttributeSet(poolAttrs)) + o.ObserveInt64(connsMax, int64(redisConf.PoolSize), metric.WithAttributeSet(poolAttrs)) - o.ObserveInt64(usage, int64(stats.IdleConns), metric.WithAttributes(idleAttrs...)) - o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributes(usedAttrs...)) + o.ObserveInt64(usage, int64(stats.IdleConns), metric.WithAttributeSet(idleAttrs)) + o.ObserveInt64(usage, int64(stats.TotalConns-stats.IdleConns), metric.WithAttributeSet(usedAttrs)) - o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributes(labels...)) - o.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributes(labels...)) - o.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributes(labels...)) + o.ObserveInt64(timeouts, int64(stats.Timeouts), metric.WithAttributeSet(poolAttrs)) + o.ObserveInt64(hits, int64(stats.Hits), metric.WithAttributeSet(poolAttrs)) + o.ObserveInt64(misses, int64(stats.Misses), metric.WithAttributeSet(poolAttrs)) return nil }, idleMax, diff --git a/extra/redisotel/metrics_test.go b/extra/redisotel/metrics_test.go new file mode 100644 index 0000000000..71a3606c26 --- /dev/null +++ b/extra/redisotel/metrics_test.go @@ -0,0 +1,54 @@ +package redisotel + +import ( + "reflect" + "testing" + + "go.opentelemetry.io/otel/attribute" +) + +func Test_poolStatsAttrs(t *testing.T) { + t.Parallel() + type args struct { + conf *config + } + tests := []struct { + name string + args args + wantPoolAttrs attribute.Set + wantIdleAttrs attribute.Set + wantUsedAttrs attribute.Set + }{ + { + name: "#3122", + args: func() args { + conf := &config{ + attrs: make([]attribute.KeyValue, 0, 4), + } + conf.attrs = append(conf.attrs, attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2")) + conf.attrs = append(conf.attrs, attribute.String("pool.name", "pool1")) + return args{conf: conf} + }(), + wantPoolAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), + attribute.String("pool.name", "pool1")), + wantIdleAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), + attribute.String("pool.name", "pool1"), attribute.String("state", "idle")), + wantUsedAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), + attribute.String("pool.name", "pool1"), attribute.String("state", "used")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPoolAttrs, gotIdleAttrs, gotUsedAttrs := poolStatsAttrs(tt.args.conf) + if !reflect.DeepEqual(gotPoolAttrs, tt.wantPoolAttrs) { + t.Errorf("poolStatsAttrs() gotPoolAttrs = %v, want %v", gotPoolAttrs, tt.wantPoolAttrs) + } + if !reflect.DeepEqual(gotIdleAttrs, tt.wantIdleAttrs) { + t.Errorf("poolStatsAttrs() gotIdleAttrs = %v, want %v", gotIdleAttrs, tt.wantIdleAttrs) + } + if !reflect.DeepEqual(gotUsedAttrs, tt.wantUsedAttrs) { + t.Errorf("poolStatsAttrs() gotUsedAttrs = %v, want %v", gotUsedAttrs, tt.wantUsedAttrs) + } + }) + } +} From 51f6fcce85c5fc67760141eec9c55b7e0c8e8215 Mon Sep 17 00:00:00 2001 From: Monkey Date: Tue, 5 Aug 2025 20:31:58 +0800 Subject: [PATCH 228/230] feat: recover addIdleConn may occur panic (#2445) * feat: recover addIdleConn may occur panic Signed-off-by: monkey92t * fix test race Signed-off-by: monkey92t --------- Signed-off-by: monkey92t Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> --- internal/pool/export_test.go | 10 ++++++++++ internal/pool/pool.go | 12 ++++++++++++ internal/pool/pool_test.go | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/internal/pool/export_test.go b/internal/pool/export_test.go index f3a65f8639..40e387c9a0 100644 --- a/internal/pool/export_test.go +++ b/internal/pool/export_test.go @@ -12,3 +12,13 @@ func (cn *Conn) SetCreatedAt(tm time.Time) { func (cn *Conn) NetConn() net.Conn { return cn.netConn } + +func (p *ConnPool) CheckMinIdleConns() { + p.connsMu.Lock() + p.checkMinIdleConns() + p.connsMu.Unlock() +} + +func (p *ConnPool) QueueLen() int { + return len(p.queue) +} diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 6d3381c9fa..9644cb85be 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -130,6 +130,18 @@ func (p *ConnPool) checkMinIdleConns() { p.idleConnsLen++ go func() { + defer func() { + if err := recover(); err != nil { + p.connsMu.Lock() + p.poolSize-- + p.idleConnsLen-- + p.connsMu.Unlock() + + p.freeTurn() + internal.Logger.Printf(context.Background(), "addIdleConn panic: %+v", err) + } + }() + err := p.addIdleConn() if err != nil && err != ErrClosed { p.connsMu.Lock() diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 05949e42fd..736323d9dd 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -361,6 +361,24 @@ var _ = Describe("race", func() { Expect(stats.TotalConns).To(Equal(uint32(opt.PoolSize))) }) + It("recover addIdleConn panic", func() { + opt := &pool.Options{ + Dialer: func(ctx context.Context) (net.Conn, error) { + panic("test panic") + }, + PoolSize: 100, + MinIdleConns: 30, + } + p := pool.NewConnPool(opt) + + p.CheckMinIdleConns() + + Eventually(func() bool { + state := p.Stats() + return state.TotalConns == 0 && state.IdleConns == 0 && p.QueueLen() == 0 + }, "3s", "50ms").Should(BeTrue()) + }) + It("wait", func() { opt := &pool.Options{ Dialer: func(ctx context.Context) (net.Conn, error) { From 990de395bd41c404755ce7b7e4fc0789ccc62066 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:50:25 +0300 Subject: [PATCH 229/230] chore(release): 9.12.0 / redis 8.2 (#3464) --- .github/actions/run-tests/action.yml | 2 +- .github/workflows/build.yml | 2 +- RELEASE-NOTES.md | 26 +++++++++++++++----------- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/hset-struct/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 15 files changed, 33 insertions(+), 29 deletions(-) diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 75b1282730..a90d46055c 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-rc2-pre" + ["8.2.x"]="8.2" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8424f63c68..cb01124817 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-rc2-pre" + ["8.2.x"]="8.2" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 1fb51275ae..478fecb2ce 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,17 +1,16 @@ # Release Notes -# 9.12.0-beta.1 (2025-08-04) +# 9.12.0 (2025-08-05) ## 🚀 Highlights -- This is a beta release for Redis 8.2 support. +- This release includes support for [Redis 8.2](https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/release-notes/redisce/redisos-8.2-release-notes/). - Introduces an experimental Query Builders for `FTSearch`, `FTAggregate` and other search commands. - Adds support for `EPSILON` option in `FT.VSIM`. -- Includes bug fixes and improvements related to search and community contributions for [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel). +- Includes bug fixes and improvements contributed by the community related to ring and [redisotel](https://github.com/redis/go-redis/tree/master/extra/redisotel). ## Changes - -- chore(github): Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458)) +- Improve stale issue workflow ([#3458](https://github.com/redis/go-redis/pull/3458)) - chore(ci): Add 8.2 rc2 pre build for CI ([#3459](https://github.com/redis/go-redis/pull/3459)) - Added new stream commands ([#3450](https://github.com/redis/go-redis/pull/3450)) - feat: Add "skip_verify" to Sentinel ([#3428](https://github.com/redis/go-redis/pull/3428)) @@ -21,19 +20,25 @@ ## 🚀 New Features +- feat: recover addIdleConn may occur panic ([#2445](https://github.com/redis/go-redis/pull/2445)) +- feat(ring): specify custom health check func via HeartbeatFn option ([#2940](https://github.com/redis/go-redis/pull/2940)) - Add Query Builder for RediSearch commands ([#3436](https://github.com/redis/go-redis/pull/3436)) -- Add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453)) +- add configurable buffer sizes for Redis connections ([#3453](https://github.com/redis/go-redis/pull/3453)) - Add VAMANA vector type to RediSearch ([#3449](https://github.com/redis/go-redis/pull/3449)) - VSIM add `EPSILON` option ([#3454](https://github.com/redis/go-redis/pull/3454)) - Add closing support to otel metrics instrumentation ([#3444](https://github.com/redis/go-redis/pull/3444)) ## 🐛 Bug Fixes +- fix(redisotel): fix buggy append in reportPoolStats ([#3122](https://github.com/redis/go-redis/pull/3122)) - fix(search): return results even if doc is empty ([#3457](https://github.com/redis/go-redis/pull/3457)) - [ISSUE-3402]: Ring.Pipelined return dial timeout error ([#3403](https://github.com/redis/go-redis/pull/3403)) ## 🧰 Maintenance +- Merges stale issues jobs into one job with two steps ([#3463](https://github.com/redis/go-redis/pull/3463)) +- improve code readability ([#3446](https://github.com/redis/go-redis/pull/3446)) +- chore(release): 9.12.0-beta.1 ([#3460](https://github.com/redis/go-redis/pull/3460)) - DOC-5472 time series doc examples ([#3443](https://github.com/redis/go-redis/pull/3443)) - Add VAMANA compression algorithm tests ([#3461](https://github.com/redis/go-redis/pull/3461)) - bumped redis 8.2 version used in the CI/CD ([#3451](https://github.com/redis/go-redis/pull/3451)) @@ -41,13 +46,12 @@ ## Contributors We'd like to thank all the contributors who worked on this release! -[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn) and [@smnvdev](https://github.com/smnvdev) +[@andy-stark-redis](https://github.com/andy-stark-redis), [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@htemelski-redis](https://github.com/htemelski-redis), [@jouir](https://github.com/jouir), [@monkey92t](https://github.com/monkey92t), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@rokn](https://github.com/rokn), [@smnvdev](https://github.com/smnvdev), [@strobil](https://github.com/strobil) and [@wzy9607](https://github.com/wzy9607) ## New Contributors -* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in https://github.com/redis/go-redis/pull/3409 -* [@smnvdev](https://github.com/smnvdev) made their first contribution in https://github.com/redis/go-redis/pull/3403 -* [@rokn](https://github.com/rokn) made their first contribution in https://github.com/redis/go-redis/pull/3444 - +* [@htemelski-redis](https://github.com/htemelski-redis) made their first contribution in [#3409](https://github.com/redis/go-redis/pull/3409) +* [@smnvdev](https://github.com/smnvdev) made their first contribution in [#3403](https://github.com/redis/go-redis/pull/3403) +* [@rokn](https://github.com/rokn) made their first contribution in [#3444](https://github.com/redis/go-redis/pull/3444) # 9.11.0 (2025-06-24) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index fb2fc1cb4b..eea7422bae 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index d2f59e10c4..c3184464c8 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.12.0-beta.1 +require github.com/redis/go-redis/v9 v9.12.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index af085d3477..950bda59e8 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0 ) require ( diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 71bced330d..57b2ab3519 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.12.0-beta.1 +require github.com/redis/go-redis/v9 v9.12.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index ac33ff812c..bcb7c7a1a3 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -11,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.12.0-beta.1 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.12.0 + github.com/redis/go-redis/v9 v9.12.0 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -25,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index c0542a24d4..f6e190957a 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.12.0-beta.1 +require github.com/redis/go-redis/v9 v9.12.0 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index af085d3477..950bda59e8 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index dfe2a8d2ee..eed921cf76 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0 + github.com/redis/go-redis/v9 v9.12.0 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 2760c09a71..c4d387c7f6 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 644fa6808a..c28fa718f0 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0-beta.1 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.12.0 + github.com/redis/go-redis/v9 v9.12.0 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index b4e9304fa2..6864f733a9 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.12.0-beta.1 + github.com/redis/go-redis/v9 v9.12.0 ) require ( diff --git a/version.go b/version.go index 0b213b0657..4a528cad61 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.12.0-beta.1" + return "9.12.0" } From db309cfe6034ca4359a6e229130b8eacd7628698 Mon Sep 17 00:00:00 2001 From: Nic Gibson Date: Mon, 11 Aug 2025 01:54:01 +0300 Subject: [PATCH 230/230] Ensure that JSON.GET returns Nil response Updated JSONCmd.readReply to return redis.Nil if no results. Added a doc line for Val() and Expanded() methods of JSONCmd. Added a test case for non existent keys in json_test.go. Original-PR: #2987 --- json.go | 10 +++++++++- json_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/json.go b/json.go index 5dd34d8c50..234ee8620b 100644 --- a/json.go +++ b/json.go @@ -114,9 +114,14 @@ func (cmd *JSONCmd) Expanded() (interface{}, error) { } func (cmd *JSONCmd) readReply(rd *proto.Reader) error { + // nil response from JSON.(M)GET (cmd.baseCmd.err will be "redis: nil") + if cmd.baseCmd.Err() == Nil { + cmd.val = "" + return Nil + } + // Handle other base command errors if cmd.baseCmd.Err() != nil { - cmd.val = "" return cmd.baseCmd.Err() } @@ -128,7 +133,10 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error { if err != nil { return err } + + // Empty array could indicate no results found for JSON path if size == 0 { + cmd.val = "" return Nil } diff --git a/json_test.go b/json_test.go index f8385b205f..4e696c38dc 100644 --- a/json_test.go +++ b/json_test.go @@ -263,10 +263,6 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res).To(Equal("OK")) - _, err = client.JSONGet(ctx, "this-key-does-not-exist", "$").Result() - Expect(err).To(HaveOccurred()) - Expect(err).To(BeIdenticalTo(redis.Nil)) - resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() Expect(err).NotTo(HaveOccurred()) Expect(resArr).To(Equal([]int64{1, 2})) @@ -681,6 +677,54 @@ var _ = Describe("JSON Commands", Label("json"), func() { Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) }) }) + + Describe("JSON Nil Handling", func() { + It("should return redis.Nil for non-existent key", func() { + _, err := client.JSONGet(ctx, "non-existent-key", "$").Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should return redis.Nil for non-existent path in existing key", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"a": 1, "b": "hello"}`).Err() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.JSONGet(ctx, "test-key", "$.nonexistent").Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should distinguish empty array from nil", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"arr": [], "obj": {}}`).Err() + Expect(err).NotTo(HaveOccurred()) + + // Empty array should return the array, not nil + val, err := client.JSONGet(ctx, "test-key", "$.arr").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[[]]")) + + // Non-existent field should return nil + _, err = client.JSONGet(ctx, "test-key", "$.missing").Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should handle multiple paths with mixed results", func() { + err := client.JSONSet(ctx, "test-key", "$", `{"a": 1, "b": 2}`).Err() + Expect(err).NotTo(HaveOccurred()) + + // Path that exists + val, err := client.JSONGet(ctx, "test-key", "$.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("[1]")) + + // Path that doesn't exist + _, err = client.JSONGet(ctx, "test-key", "$.c").Result() + Expect(err).To(Equal(redis.Nil)) + }) + + AfterEach(func() { + // Clean up test keys + client.Del(ctx, "test-key", "non-existent-key") + }) + }) } })