From 482a20d9c970c7ba99181290586ca1c9122a5478 Mon Sep 17 00:00:00 2001 From: Nikolay Dubina Date: Tue, 25 Mar 2025 15:28:25 +0800 Subject: [PATCH 01/25] 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 1fc34f73..578616b9 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 335d32da..fc64e8dd 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 cb3aa1961833d2e5beae39d72c9eafc7d9364957 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 25 Mar 2025 12:25:35 +0200 Subject: [PATCH 02/25] 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 fc64e8dd..6c42c0fc 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 0699671c..727fbbd7 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 72ab52aa..775e3e7b 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 f14f54df..33d3ef6d 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 e2bc161a..363c93c2 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 299e4b02..5a060d99 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 04494565..3d7a4caa 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 f4453607..33d3ef6d 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 11230414..7033e805 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 d64ad570..c1cff3e9 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 13899eb5..e5b442e6 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 fa3c43ae..8bff0008 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 a4832fc1..b5479516 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 53daf77e1ae625800a786655b898bdcb7aeaa9d0 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 03/25] 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 00000000..ee4a40a0 --- /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 0cfcd1a61de06e00798b50ae3571e876e11bdede Mon Sep 17 00:00:00 2001 From: Liu Shuang Date: Thu, 3 Apr 2025 21:10:31 +0800 Subject: [PATCH 04/25] update pubsub.go (#3329) --- pubsub.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubsub.go b/pubsub.go index 72b18f49..20c085f1 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 0df0f3c88785aa16c996089b68b731a4e4a2fdc0 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 3 Apr 2025 16:10:51 +0300 Subject: [PATCH 05/25] 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 def48baf..2edb16d3 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 48bbdb75..f88ca672 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 296f5bd8..4359b02f 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 8e020c1d8fd29aa282ab5f0256a2f7b596b3e6d8 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 3 Apr 2025 17:01:34 +0300 Subject: [PATCH 06/25] 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 8be39d2a..85e12561 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 4359b02f..4d8417d7 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 46484324a51770d4e39cad4838e6d0ec24e391cc 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 07/25] 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 beefa616..4d0fc338 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 6e071774950218201ee9c86cb19102a7070431b5 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 08/25] 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 85e12561..40942620 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 4d8417d7..3c4457a4 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 93bc3e6140b104870d5cea26e944f91b9b18925a 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 09/25] 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 1f53f344..50d94bae 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 eedb171825167a733d798ee3ae75f364cef53ffc Mon Sep 17 00:00:00 2001 From: Bulat Khasanov Date: Tue, 15 Apr 2025 16:57:50 +0300 Subject: [PATCH 10/25] 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 a4c9f53c..a132af2f 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 b34706f8..07c7628a 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 b26758a906defe586a5f5b5db36f5e07d26d8bb2 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 11/25] 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 ec2843ad..4ee93d79 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 7b9bd6c36e3696e619a9fc5880d5fff002ed32a0 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 12/25] 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 40942620..b31baaa7 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 3c4457a4..6bc8b111 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 e2149b06f767f0b9968d3c7212bf8831bbea6ab4 Mon Sep 17 00:00:00 2001 From: Bulat Khasanov Date: Wed, 16 Apr 2025 18:32:57 +0300 Subject: [PATCH 13/25] 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 a6317196..516ada82 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 b018cc9e..20180464 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 a4aea258fce4f9469b61ae677917faa90eed1445 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 14/25] 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 a132af2f..06386635 100644 --- a/sentinel.go +++ b/sentinel.go @@ -566,29 +566,60 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { } } + var ( + masterAddr string + wg sync.WaitGroup + once sync.Once + errCh = make(chan error, len(c.sentinelAddrs)) + ) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for i, sentinelAddr := range c.sentinelAddrs { - sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr)) - - 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 + 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 } - internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s", - c.opt.MasterName, err) - continue - } - // Push working sentinel to the top. - c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0] - c.setSentinel(ctx, sentinel) - - 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 07c7628a..cde7f956 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 e191cf9dbe6e28e2cec5cde18207ea6605116cdc 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 15/25] 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 06386635..f5b9a52d 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 a7b4ce5e508cf1df892383bf92ca3af7e8504b0e Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 21 Apr 2025 22:11:00 -0700 Subject: [PATCH 16/25] 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 69650145..364706e3 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 6321c15e..123005bc 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 50d94bae..be58b8d2 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 f7780dddac4d556c9ab8a3ec37ab2737ffc1a1a3 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 17/25] 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 4ee93d79..f32bf8d1 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 034a49f88a6ef46fa357401453818337feec212b Mon Sep 17 00:00:00 2001 From: frankj Date: Wed, 23 Apr 2025 15:47:13 +0800 Subject: [PATCH 18/25] 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 123005bc..bca7d7ee 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 55b95749..6a76756a 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 030c184a71c2728ffe9c835fbaf428c53cd682f1 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 19/25] 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 bcaee7c7..7228a4a0 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 6c42c0fc..4487c6e9 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 182a04f5fa3decb6975dfb2fb7281aff13dcb98d 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 20/25] 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 be58b8d2..98a361b3 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 4bd5d417ca593abff98bbb49cbb6354e6f9bca6f Mon Sep 17 00:00:00 2001 From: Hui Date: Tue, 29 Apr 2025 05:16:53 +0800 Subject: [PATCH 21/25] 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 bca7d7ee..27132324 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 6a76756a..8b2aa37d 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 (s *numberStruct) UnmarshalBinary(b []byte) error { - return json.Unmarshal(b, s) +func (n *numberStruct) UnmarshalBinary(b []byte) error { + return json.Unmarshal(b, n) +} + +func (n *numberStruct) ScanRedis(str string) error { + return json.Unmarshal([]byte(str), n) } func deref(viface interface{}) interface{} { From bb8d50848185fe660c7f0acc3eec5aeebb5f3727 Mon Sep 17 00:00:00 2001 From: fukua95 Date: Tue, 29 Apr 2025 14:39:26 +0800 Subject: [PATCH 22/25] fix: `PubSub` isn't concurrency-safe (#3360) --- pubsub.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pubsub.go b/pubsub.go index 20c085f1..2a0e7a81 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 2f0a9b720a1cde3cae709ec3423498177b3b0ee7 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 23/25] 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 515750af..5e0ac1d0 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 285aca6b..872454ff 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 364706e3..3253af6c 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 7033e805..b39f7dd4 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 c1cff3e9..93cc423d 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 e5b442e6..c5b29dff 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 8bff0008..c934767e 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 0ebeec34..3ffcd07e 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 20180464..3b46cbe3 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 0ff3f75b..8f2dd3c4 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 cde7f956..2d481d5f 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 3d91dd49..46d5640d 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 683f644ec2d8bd82a3df9e9f62c9ee3e7d59373c 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 24/25] 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 2edb16d3..08323aa5 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 f88ca672..810ab509 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 d0d865b4..fdef3e60 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 d54e848055afe1ba941931048087fb043a02aa58 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 25/25] 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 3b46cbe3..c0278ed0 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 e0159294..f50df568 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 7d9bf1ce..80e28341 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 8f2dd3c4..555ea2a1 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 f5b9a52d..cfc848cf 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 46d5640d..a1ce17ba 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())