From 69b4c0165fc7981144e5cfe3b3d028827f4e82a0 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:54:20 +0100 Subject: [PATCH 01/68] DOC-4241 added t-digest examples (#3123) --- doctests/tdigest_tutorial_test.go | 251 ++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 doctests/tdigest_tutorial_test.go diff --git a/doctests/tdigest_tutorial_test.go b/doctests/tdigest_tutorial_test.go new file mode 100644 index 000000000..7589b0ec8 --- /dev/null +++ b/doctests/tdigest_tutorial_test.go @@ -0,0 +1,251 @@ +// EXAMPLE: tdigest_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_tdigstart() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages", "bikes:sales") + // REMOVE_END + + // STEP_START tdig_start + res1, err := rdb.TDigestCreate(ctx, "bikes:sales").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TDigestAdd(ctx, "bikes:sales", 21).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> OK + + res3, err := rdb.TDigestAdd(ctx, "bikes:sales", + 150, 95, 75, 34, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> OK + + // STEP_END + + // Output: + // OK + // OK + // OK +} + +func ExampleClient_tdigcdf() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages", "bikes:sales") + // REMOVE_END + + // STEP_START tdig_cdf + res4, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> OK + + res5, err := rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> OK + + res6, err := rdb.TDigestRank(ctx, "racer_ages", 50).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> [7] + + res7, err := rdb.TDigestRank(ctx, "racer_ages", 50, 40).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> [7 4] + // STEP_END + + // Output: + // OK + // OK + // [7] + // [7 4] +} + +func ExampleClient_tdigquant() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_quant + res8, err := rdb.TDigestQuantile(ctx, "racer_ages", 0.5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> [44.2] + + res9, err := rdb.TDigestByRank(ctx, "racer_ages", 4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [42.63] + // STEP_END + + // Output: + // [44.2] + // [42.63] +} + +func ExampleClient_tdigmin() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + _, err = rdb.TDigestAdd(ctx, "racer_ages", + 45.88, 44.2, 58.03, 19.76, 39.84, 69.28, + 50.97, 25.41, 19.27, 85.71, 42.63, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_min + res10, err := rdb.TDigestMin(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> 19.27 + + res11, err := rdb.TDigestMax(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 85.71 + // STEP_END + + // Output: + // 19.27 + // 85.71 +} + +func ExampleClient_tdigreset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "racer_ages") + // REMOVE_END + _, err := rdb.TDigestCreate(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + // STEP_START tdig_reset + res12, err := rdb.TDigestReset(ctx, "racer_ages").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> OK + // STEP_END + + // Output: + // OK +} From 9e3709c4040acfa504bb7803dba709e70e780dc9 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:03:54 +0100 Subject: [PATCH 02/68] DOC-4234 added bitmap examples (#3124) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/bitmap_tutorial_test.go | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 doctests/bitmap_tutorial_test.go diff --git a/doctests/bitmap_tutorial_test.go b/doctests/bitmap_tutorial_test.go new file mode 100644 index 000000000..dbfc247ac --- /dev/null +++ b/doctests/bitmap_tutorial_test.go @@ -0,0 +1,92 @@ +// EXAMPLE: bitmap_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_ping() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "pings:2024-01-01-00:00") + // REMOVE_END + + // STEP_START ping + res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 0 + + res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 1 + + res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 0 + // STEP_END + + // Output: + // 0 + // 1 + // 0 +} + +func ExampleClient_bitcount() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() + + if err != nil { + panic(err) + } + // REMOVE_END + + // STEP_START bitcount + res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00", + &redis.BitCount{ + Start: 0, + End: 456, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> 1 + // STEP_END + + // Output: + // 1 +} From 9e79c9bd394cb62efb0e00954a9014c09f0aeaa6 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:24:21 +0100 Subject: [PATCH 03/68] DOC-4228 JSON code examples (#3114) * DOC-4228 added JSON code examples * DOC-4228 example and formatting corrections --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/json_tutorial_test.go | 1149 ++++++++++++++++++++++++++++++++ 1 file changed, 1149 insertions(+) create mode 100644 doctests/json_tutorial_test.go diff --git a/doctests/json_tutorial_test.go b/doctests/json_tutorial_test.go new file mode 100644 index 000000000..4e9787330 --- /dev/null +++ b/doctests/json_tutorial_test.go @@ -0,0 +1,1149 @@ +// EXAMPLE: json_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END +func ExampleClient_setget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike") + // REMOVE_END + + // STEP_START set_get + res1, err := rdb.JSONSet(ctx, "bike", "$", + "\"Hyperion\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.JSONGet(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> ["Hyperion"] + + res3, err := rdb.JSONType(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [[string]] + // STEP_END + + // Output: + // OK + // ["Hyperion"] + // [[string]] +} + +func ExampleClient_str() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bike", "$", + "\"Hyperion\"", + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START str + res4, err := rdb.JSONStrLen(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res4[0]) // >>> 8 + + res5, err := rdb.JSONStrAppend(ctx, "bike", "$", "\" (Enduro bikes)\"").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res5[0]) // >>> 23 + + res6, err := rdb.JSONGet(ctx, "bike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> ["Hyperion (Enduro bikes)"] + // STEP_END + + // Output: + // 8 + // 23 + // ["Hyperion (Enduro bikes)"] +} + +func ExampleClient_num() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "crashes") + // REMOVE_END + + // STEP_START num + res7, err := rdb.JSONSet(ctx, "crashes", "$", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> OK + + res8, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> [1] + + res9, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", 1.5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> [2.5] + + res10, err := rdb.JSONNumIncrBy(ctx, "crashes", "$", -0.75).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> [1.75] + // STEP_END + + // Output: + // OK + // [1] + // [2.5] + // [1.75] +} + +func ExampleClient_arr() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "newbike") + // REMOVE_END + + // STEP_START arr + res11, err := rdb.JSONSet(ctx, "newbike", "$", + []interface{}{ + "Deimos", + map[string]interface{}{"crashes": 0}, + nil, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> OK + + res12, err := rdb.JSONGet(ctx, "newbike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> [["Deimos",{"crashes":0},null]] + + res13, err := rdb.JSONGet(ctx, "newbike", "$[1].crashes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> [0] + + res14, err := rdb.JSONDel(ctx, "newbike", "$.[-1]").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> 1 + + res15, err := rdb.JSONGet(ctx, "newbike", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [["Deimos",{"crashes":0}]] + // STEP_END + + // Output: + // OK + // [["Deimos",{"crashes":0},null]] + // [0] + // 1 + // [["Deimos",{"crashes":0}]] +} + +func ExampleClient_arr2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "riders") + // REMOVE_END + + // STEP_START arr2 + res16, err := rdb.JSONSet(ctx, "riders", "$", []interface{}{}).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> OK + + res17, err := rdb.JSONArrAppend(ctx, "riders", "$", "\"Norem\"").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [1] + + res18, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> [["Norem"]] + + res19, err := rdb.JSONArrInsert(ctx, "riders", "$", 1, + "\"Prickett\"", "\"Royce\"", "\"Castilla\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // [3] + + res20, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> [["Norem", "Prickett", "Royce", "Castilla"]] + + rangeStop := 1 + + res21, err := rdb.JSONArrTrimWithArgs(ctx, "riders", "$", + &redis.JSONArrTrimArgs{Start: 1, Stop: &rangeStop}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) // >>> [1] + + res22, err := rdb.JSONGet(ctx, "riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) // >>> [["Prickett"]] + + res23, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> [["Prickett"]] + + res24, err := rdb.JSONArrPop(ctx, "riders", "$", -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) // [] + // STEP_END + + // Output: + // OK + // [1] + // [["Norem"]] + // [4] + // [["Norem","Prickett","Royce","Castilla"]] + // [1] + // [["Prickett"]] + // ["Prickett"] + // [] +} + +func ExampleClient_obj() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1") + // REMOVE_END + + // STEP_START obj + res25, err := rdb.JSONSet(ctx, "bike:1", "$", + map[string]interface{}{ + "model": "Deimos", + "brand": "Ergonom", + "price": 4972, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) // >>> OK + + res26, err := rdb.JSONObjLen(ctx, "bike:1", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(*res26[0]) // >>> 3 + + res27, err := rdb.JSONObjKeys(ctx, "bike:1", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res27) // >>> [brand model price] + // STEP_END + + // Output: + // OK + // 3 + // [[brand model price]] +} + +var inventory_json = map[string]interface{}{ + "inventory": map[string]interface{}{ + "mountain_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic " + + "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " + + "gives plenty of gear range to tackle hills and there\u2019s room for " + + "mudguards and a rack too. This is the bike for the rider who wants " + + "trail manners with low fuss ownership.", + "price": 1920, + "specs": map[string]interface{}{"material": "carbon", "weight": 13.1}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike " + + "impressed our testers and is the best all-around trail bike we've " + + "ever tested. The Shimano gear system effectively does away with an " + + "external cassette, so is super low maintenance in terms of wear " + + "and tear. All in all it's an impressive package for the price, " + + "making it very competitive.", + "price": 2072, + "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9}, + "colors": []interface{}{"black", "white"}, + }, + map[string]interface{}{ + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first experience " + + "on tracks and easy cruising through forests and fields. A set of " + + "powerful Shimano hydraulic disc brakes provide ample stopping " + + "ability. If you're after a budget option, this is one of the best " + + "bikes you could get.", + "price": 3264, + "specs": map[string]interface{}{"material": "alloy", "weight": 13.8}, + }, + }, + "commuter_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:4", + "model": "Salacia", + "description": "This bike is a great option for anyone who just " + + "wants a bike to get about on With a slick-shifting Claris gears " + + "from Shimano\u2019s, this is a bike which doesn\u2019t break the " + + "bank and delivers craved performance. It\u2019s for the rider " + + "who wants both efficiency and capability.", + "price": 1475, + "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:5", + "model": "Mimas", + "description": "A real joy to ride, this bike got very high " + + "scores in last years Bike of the year report. The carefully " + + "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " + + "easy-on-the-legs bottom gear for climbing, and the high-quality " + + "Vittoria Zaffiro tires give balance and grip.It includes " + + "a low-step frame , our memory foam seat, bump-resistant shocks and " + + "conveniently placed thumb throttle. Put it all together and you " + + "get a bike that helps redefine what can be done for this price.", + "price": 3941, + "specs": map[string]interface{}{"material": "alloy", "weight": 11.6}, + }, + }, + }, +} + +func ExampleClient_setbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + // STEP_START set_bikes + var inventory_json = map[string]interface{}{ + "inventory": map[string]interface{}{ + "mountain_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic " + + "daily driver or one bike quiver. The Shimano Claris 8-speed groupset " + + "gives plenty of gear range to tackle hills and there\u2019s room for " + + "mudguards and a rack too. This is the bike for the rider who wants " + + "trail manners with low fuss ownership.", + "price": 1920, + "specs": map[string]interface{}{"material": "carbon", "weight": 13.1}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike " + + "impressed our testers and is the best all-around trail bike we've " + + "ever tested. The Shimano gear system effectively does away with an " + + "external cassette, so is super low maintenance in terms of wear " + + "and tear. All in all it's an impressive package for the price, " + + "making it very competitive.", + "price": 2072, + "specs": map[string]interface{}{"material": "aluminium", "weight": 7.9}, + "colors": []interface{}{"black", "white"}, + }, + map[string]interface{}{ + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older " + + "a durable and uberlight mountain bike for their first experience " + + "on tracks and easy cruising through forests and fields. A set of " + + "powerful Shimano hydraulic disc brakes provide ample stopping " + + "ability. If you're after a budget option, this is one of the best " + + "bikes you could get.", + "price": 3264, + "specs": map[string]interface{}{"material": "alloy", "weight": 13.8}, + }, + }, + "commuter_bikes": []interface{}{ + map[string]interface{}{ + "id": "bike:4", + "model": "Salacia", + "description": "This bike is a great option for anyone who just " + + "wants a bike to get about on With a slick-shifting Claris gears " + + "from Shimano\u2019s, this is a bike which doesn\u2019t break the " + + "bank and delivers craved performance. It\u2019s for the rider " + + "who wants both efficiency and capability.", + "price": 1475, + "specs": map[string]interface{}{"material": "aluminium", "weight": 16.6}, + "colors": []interface{}{"black", "silver"}, + }, + map[string]interface{}{ + "id": "bike:5", + "model": "Mimas", + "description": "A real joy to ride, this bike got very high " + + "scores in last years Bike of the year report. The carefully " + + "crafted 50-34 tooth chainset and 11-32 tooth cassette give an " + + "easy-on-the-legs bottom gear for climbing, and the high-quality " + + "Vittoria Zaffiro tires give balance and grip.It includes " + + "a low-step frame , our memory foam seat, bump-resistant shocks and " + + "conveniently placed thumb throttle. Put it all together and you " + + "get a bike that helps redefine what can be done for this price.", + "price": 3941, + "specs": map[string]interface{}{"material": "alloy", "weight": 11.6}, + }, + }, + }, + } + + res1, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_getbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_bikes + res2, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory", + &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "}, + "$.inventory.*", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) + // >>> + // [ + // [ + // { + // "colors": [ + // "black", + // "silver" + // ... + // STEP_END + + // Output: + // [ + // [ + // { + // "colors": [ + // "black", + // "silver" + // ], + // "description": "This bike is a great option for anyone who just wants a bike to get about on With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance. It’s for the rider who wants both efficiency and capability.", + // "id": "bike:4", + // "model": "Salacia", + // "price": 1475, + // "specs": { + // "material": "aluminium", + // "weight": 16.6 + // } + // }, + // { + // "description": "A real joy to ride, this bike got very high scores in last years Bike of the year report. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.", + // "id": "bike:5", + // "model": "Mimas", + // "price": 3941, + // "specs": { + // "material": "alloy", + // "weight": 11.6 + // } + // } + // ], + // [ + // { + // "colors": [ + // "black", + // "silver" + // ], + // "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.", + // "id": "bike:1", + // "model": "Phoebe", + // "price": 1920, + // "specs": { + // "material": "carbon", + // "weight": 13.1 + // } + // }, + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + // "id": "bike:2", + // "model": "Quaoar", + // "price": 2072, + // "specs": { + // "material": "aluminium", + // "weight": 7.9 + // } + // }, + // { + // "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + // "id": "bike:3", + // "model": "Weywot", + // "price": 3264, + // "specs": { + // "material": "alloy", + // "weight": 13.8 + // } + // } + // ] + // ] +} + +func ExampleClient_getmtnbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_mtnbikes + res3, err := rdb.JSONGet(ctx, "bikes:inventory", + "$.inventory.mountain_bikes[*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) + // >>> ["Phoebe","Quaoar","Weywot"] + + res4, err := rdb.JSONGet(ctx, + "bikes:inventory", "$.inventory[\"mountain_bikes\"][*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> ["Phoebe","Quaoar","Weywot"] + + res5, err := rdb.JSONGet(ctx, + "bikes:inventory", "$..mountain_bikes[*].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> ["Phoebe","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Phoebe","Quaoar","Weywot"] + // ["Phoebe","Quaoar","Weywot"] + // ["Phoebe","Quaoar","Weywot"] +} + +func ExampleClient_getmodels() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get_models + res6, err := rdb.JSONGet(ctx, "bikes:inventory", "$..model").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) // >>> ["Salacia","Mimas","Phoebe","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Salacia","Mimas","Phoebe","Quaoar","Weywot"] +} + +func ExampleClient_get2mtnbikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START get2mtnbikes + res7, err := rdb.JSONGet(ctx, "bikes:inventory", "$..mountain_bikes[0:2].model").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> ["Phoebe","Quaoar"] + // STEP_END + + // Output: + // ["Phoebe","Quaoar"] +} + +func ExampleClient_filter1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter1 + res8, err := rdb.JSONGetWithArgs(ctx, "bikes:inventory", + &redis.JSONGetArgs{Indent: " ", Newline: "\n", Space: " "}, + "$..mountain_bikes[?(@.price < 3000 && @.specs.weight < 10)]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) + // >>> + // [ + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year + // ... + // STEP_END + + // Output: + // [ + // { + // "colors": [ + // "black", + // "white" + // ], + // "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + // "id": "bike:2", + // "model": "Quaoar", + // "price": 2072, + // "specs": { + // "material": "aluminium", + // "weight": 7.9 + // } + // } + // ] +} + +func ExampleClient_filter2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter2 + res9, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$..[?(@.specs.material == 'alloy')].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> ["Mimas","Weywot"] + // STEP_END + + // Output: + // ["Mimas","Weywot"] +} + +func ExampleClient_filter3() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter3 + res10, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$..[?(@.specs.material =~ '(?i)al')].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res10) // >>> ["Salacia","Mimas","Quaoar","Weywot"] + // STEP_END + + // Output: + // ["Salacia","Mimas","Quaoar","Weywot"] +} + +func ExampleClient_filter4() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START filter4 + res11, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[0].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> OK + + res12, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[1].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) // >>> OK + + res13, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[2].regex_pat", + "\"(?i)al\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) // >>> OK + + res14, err := rdb.JSONGet(ctx, + "bikes:inventory", + "$.inventory.mountain_bikes[?(@.specs.material =~ @.regex_pat)].model", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) // >>> ["Quaoar","Weywot"] + // STEP_END + + // Output: + // OK + // OK + // OK + // ["Quaoar","Weywot"] +} + +func ExampleClient_updatebikes() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_bikes + res15, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) // >>> [1475,3941,1920,2072,3264] + + res16, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", -100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) // >>> [1375,3841,1820,1972,3164] + + res17, err := rdb.JSONNumIncrBy(ctx, "bikes:inventory", "$..price", 100).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) // >>> [1475,3941,1920,2072,3264] + // STEP_END + + // Output: + // [1475,3941,1920,2072,3264] + // [1375,3841,1820,1972,3164] + // [1475,3941,1920,2072,3264] +} + +func ExampleClient_updatefilters1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_filters1 + res18, err := rdb.JSONSet(ctx, + "bikes:inventory", + "$.inventory.*[?(@.price<2000)].price", + 1500, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) // >>> OK + + res19, err := rdb.JSONGet(ctx, "bikes:inventory", "$..price").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> [1500,3941,1500,2072,3264] + // STEP_END + + // Output: + // OK + // [1500,3941,1500,2072,3264] +} + +func ExampleClient_updatefilters2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:inventory") + // REMOVE_END + + _, err := rdb.JSONSet(ctx, "bikes:inventory", "$", inventory_json).Result() + + if err != nil { + panic(err) + } + + // STEP_START update_filters2 + res20, err := rdb.JSONArrAppend(ctx, + "bikes:inventory", + "$.inventory.*[?(@.price<2000)].colors", + "\"pink\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> [3 3] + + res21, err := rdb.JSONGet(ctx, "bikes:inventory", "$..[*].colors").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res21) + // >>> [["black","silver","pink"],["black","silver","pink"],["black","white"]] + // STEP_END + + // Output: + // [3 3] + // [["black","silver","pink"],["black","silver","pink"],["black","white"]] +} From e99abe45469cefe078a05e3c656ca452f3cce646 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:41:54 +0100 Subject: [PATCH 04/68] DOC-4237 added Bloom filter examples (#3115) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/bf_tutorial_test.go | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 doctests/bf_tutorial_test.go diff --git a/doctests/bf_tutorial_test.go b/doctests/bf_tutorial_test.go new file mode 100644 index 000000000..67545f1d5 --- /dev/null +++ b/doctests/bf_tutorial_test.go @@ -0,0 +1,83 @@ +// EXAMPLE: bf_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_bloom() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:models") + // REMOVE_END + + // STEP_START bloom + res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + res4, err := rdb.BFMAdd(ctx, "bikes:models", + "Rocky Mountain Racer", + "Cloudy City Cruiser", + "Windy City Wippet", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [true true true] + + res5, err := rdb.BFMExists(ctx, "bikes:models", + "Rocky Mountain Racer", + "Cloudy City Cruiser", + "Windy City Wippet", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> [true true true] + // STEP_END + + // Output: + // OK + // true + // true + // [true true true] + // [true true true] +} From d9eeed131a32b4aeaf1e1915b6717f8cd2af9124 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:11:59 +0300 Subject: [PATCH 05/68] Fix Flaky Test: should handle FTAggregate with Unstable RESP3 Search Module and without stability (#3135) --- go.mod | 2 +- search_test.go | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index bd13d7453..c1d9037ac 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,6 @@ require ( ) retract ( - v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. + v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) diff --git a/search_test.go b/search_test.go index 93859a4e7..efdc6bb1e 100644 --- a/search_test.go +++ b/search_test.go @@ -1446,16 +1446,18 @@ var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { options := &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawResult() - rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() - - Expect(err).NotTo(HaveOccurred()) - Expect(rawVal).To(BeEquivalentTo(res)) results := res.(map[interface{}]interface{})["results"].([]interface{}) Expect(results[0].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) Expect(results[1].(map[interface{}]interface{})["extra_attributes"].(map[interface{}]interface{})["CreatedDateTimeUTC"]). To(Or(BeEquivalentTo("6373878785249699840"), BeEquivalentTo("6373878758592700416"))) + rawVal := client.FTAggregateWithArgs(ctx, "idx1", "*", options).RawVal() + rawValResults := rawVal.(map[interface{}]interface{})["results"].([]interface{}) + Expect(err).NotTo(HaveOccurred()) + Expect(rawValResults[0]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1]))) + Expect(rawValResults[1]).To(Or(BeEquivalentTo(results[0]), BeEquivalentTo(results[1]))) + // Test with UnstableResp3 false Expect(func() { options = &redis.FTAggregateOptions{Apply: []redis.FTAggregateApply{{Field: "@CreatedDateTimeUTC * 10", As: "CreatedDateTimeUTC"}}} From e7868623ac74964c7814123ce488e2cb3f784386 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 7 Oct 2024 10:23:11 +0100 Subject: [PATCH 06/68] Remove direct read from TLS underlying conn (#3138) --- internal/pool/conn_check.go | 5 ----- internal/pool/conn_check_test.go | 18 ------------------ 2 files changed, 23 deletions(-) diff --git a/internal/pool/conn_check.go b/internal/pool/conn_check.go index 07c261c2b..83190d394 100644 --- a/internal/pool/conn_check.go +++ b/internal/pool/conn_check.go @@ -3,7 +3,6 @@ package pool import ( - "crypto/tls" "errors" "io" "net" @@ -17,10 +16,6 @@ func connCheck(conn net.Conn) error { // Reset previous timeout. _ = conn.SetDeadline(time.Time{}) - // Check if tls.Conn. - if c, ok := conn.(*tls.Conn); ok { - conn = c.NetConn() - } sysConn, ok := conn.(syscall.Conn) if !ok { return nil diff --git a/internal/pool/conn_check_test.go b/internal/pool/conn_check_test.go index 214993339..2ade8a0b9 100644 --- a/internal/pool/conn_check_test.go +++ b/internal/pool/conn_check_test.go @@ -3,7 +3,6 @@ package pool import ( - "crypto/tls" "net" "net/http/httptest" "time" @@ -15,17 +14,12 @@ import ( var _ = Describe("tests conn_check with real conns", func() { var ts *httptest.Server var conn net.Conn - var tlsConn *tls.Conn var err error BeforeEach(func() { ts = httptest.NewServer(nil) conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second) Expect(err).NotTo(HaveOccurred()) - tlsTestServer := httptest.NewUnstartedServer(nil) - tlsTestServer.StartTLS() - tlsConn, err = tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, tlsTestServer.Listener.Addr().Network(), tlsTestServer.Listener.Addr().String(), &tls.Config{InsecureSkipVerify: true}) - Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { @@ -39,23 +33,11 @@ var _ = Describe("tests conn_check with real conns", func() { Expect(connCheck(conn)).To(HaveOccurred()) }) - It("good tls conn check", func() { - Expect(connCheck(tlsConn)).NotTo(HaveOccurred()) - - Expect(tlsConn.Close()).NotTo(HaveOccurred()) - Expect(connCheck(tlsConn)).To(HaveOccurred()) - }) - It("bad conn check", func() { Expect(conn.Close()).NotTo(HaveOccurred()) Expect(connCheck(conn)).To(HaveOccurred()) }) - It("bad tls conn check", func() { - Expect(tlsConn.Close()).NotTo(HaveOccurred()) - Expect(connCheck(tlsConn)).To(HaveOccurred()) - }) - It("check conn deadline", func() { Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred()) time.Sleep(time.Millisecond * 10) From 5d03e9ec33d5547ec562d174a9856feb09417cd1 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 06:56:44 +0100 Subject: [PATCH 07/68] DOC-4238 added Cuckoo filter examples (#3116) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/cuckoo_tutorial_test.go | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/cuckoo_tutorial_test.go diff --git a/doctests/cuckoo_tutorial_test.go b/doctests/cuckoo_tutorial_test.go new file mode 100644 index 000000000..08a503b10 --- /dev/null +++ b/doctests/cuckoo_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: cuckoo_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cuckoo() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:models") + // REMOVE_END + + // STEP_START cuckoo + res1, err := rdb.CFReserve(ctx, "bikes:models", 1000000).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.CFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> true + + res3, err := rdb.CFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> true + + res4, err := rdb.CFExists(ctx, "bikes:models", "Terrible Bike Name").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> false + + res5, err := rdb.CFDel(ctx, "bikes:models", "Smoky Mountain Striker").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> true + // STEP_END + + // Output: + // OK + // true + // true + // false + // true +} From 7cc1e32e03bae4ca3cd3904b19bf88772d9ff9c5 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 07:14:34 +0100 Subject: [PATCH 08/68] DOC-4236 added HyperLogLog examples (#3117) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/hll_tutorial_test.go | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/hll_tutorial_test.go diff --git a/doctests/hll_tutorial_test.go b/doctests/hll_tutorial_test.go new file mode 100644 index 000000000..57e78d108 --- /dev/null +++ b/doctests/hll_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: hll_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_pfadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes") + // REMOVE_END + + // STEP_START pfadd + res1, err := rdb.PFAdd(ctx, "bikes", "Hyperion", "Deimos", "Phoebe", "Quaoar").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // 1 + + res2, err := rdb.PFCount(ctx, "bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // 4 + + res3, err := rdb.PFAdd(ctx, "commuter_bikes", "Salacia", "Mimas", "Quaoar").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // 1 + + res4, err := rdb.PFMerge(ctx, "all_bikes", "bikes", "commuter_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // OK + + res5, err := rdb.PFCount(ctx, "all_bikes").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // 6 + // STEP_END + + // Output: + // 1 + // 4 + // 1 + // OK + // 6 +} From 13682c437e37e87240141f3618db6fdb4bc4a59d Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:24:47 +0100 Subject: [PATCH 09/68] DOC-4239 added Count-min sketch examples (#3118) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cms_tutorial_test.go | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 doctests/cms_tutorial_test.go diff --git a/doctests/cms_tutorial_test.go b/doctests/cms_tutorial_test.go new file mode 100644 index 000000000..ade1fa93d --- /dev/null +++ b/doctests/cms_tutorial_test.go @@ -0,0 +1,84 @@ +// EXAMPLE: cms_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cms() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:profit") + // REMOVE_END + + // STEP_START cms + res1, err := rdb.CMSInitByProb(ctx, "bikes:profit", 0.001, 0.002).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.CMSIncrBy(ctx, "bikes:profit", + "Smoky Mountain Striker", 100, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [100] + + res3, err := rdb.CMSIncrBy(ctx, "bikes:profit", + "Rocky Mountain Racer", 200, + "Cloudy City Cruiser", 150, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [200 150] + + res4, err := rdb.CMSQuery(ctx, "bikes:profit", + "Smoky Mountain Striker", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [100] + + res5, err := rdb.CMSInfo(ctx, "bikes:profit").Result() + + if err != nil { + panic(err) + } + + fmt.Printf("Width: %v, Depth: %v, Count: %v", + res5.Width, res5.Depth, res5.Count) + // >>> Width: 2000, Depth: 9, Count: 450 + // STEP_END + + // Output: + // OK + // [100] + // [200 150] + // [100] + // Width: 2000, Depth: 9, Count: 450 +} From e3d41f20f728f7f9fa4d834136d9d4dd3a7a328b Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:31:14 +0100 Subject: [PATCH 10/68] DOC-4235 added bitfield examples (#3125) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/bitfield_tutorial_test.go | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 doctests/bitfield_tutorial_test.go diff --git a/doctests/bitfield_tutorial_test.go b/doctests/bitfield_tutorial_test.go new file mode 100644 index 000000000..04fcb35f2 --- /dev/null +++ b/doctests/bitfield_tutorial_test.go @@ -0,0 +1,79 @@ +// EXAMPLE: bitfield_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_bf() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bike:1:stats") + // REMOVE_END + + // STEP_START bf + res1, err := rdb.BitField(ctx, "bike:1:stats", + "set", "u32", "#0", "1000", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> [0] + + res2, err := rdb.BitField(ctx, + "bike:1:stats", + "incrby", "u32", "#0", "-50", + "incrby", "u32", "#1", "1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [950 1] + + res3, err := rdb.BitField(ctx, + "bike:1:stats", + "incrby", "u32", "#0", "500", + "incrby", "u32", "#1", "1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> [1450 2] + + res4, err := rdb.BitField(ctx, "bike:1:stats", + "get", "u32", "#0", + "get", "u32", "#1", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [1450 2] + // STEP_END + + // Output: + // [0] + // [950 1] + // [1450 2] + // [1450 2] +} From bef6b10d6f85b50e5964754d678781e10654e3e0 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:33:46 +0100 Subject: [PATCH 11/68] DOC-4240 added Top-K examples (#3119) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/topk_tutorial_test.go | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doctests/topk_tutorial_test.go diff --git a/doctests/topk_tutorial_test.go b/doctests/topk_tutorial_test.go new file mode 100644 index 000000000..2d1fe7fc2 --- /dev/null +++ b/doctests/topk_tutorial_test.go @@ -0,0 +1,75 @@ +// EXAMPLE: topk_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_topk() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:keywords") + // REMOVE_END + + // STEP_START topk + res1, err := rdb.TopKReserve(ctx, "bikes:keywords", 5).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> OK + + res2, err := rdb.TopKAdd(ctx, "bikes:keywords", + "store", + "seat", + "handlebars", + "handles", + "pedals", + "tires", + "store", + "seat", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> [ handlebars ] + + res3, err := rdb.TopKList(ctx, "bikes:keywords").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // [store seat pedals tires handles] + + res4, err := rdb.TopKQuery(ctx, "bikes:keywords", "store", "handlebars").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // [true false] + // STEP_END + + // Output: + // OK + // [ handlebars ] + // [store seat pedals tires handles] + // [true false] +} From 7fc46e206b0edc472651652f5b92447279685f3e Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:36:42 +0100 Subject: [PATCH 12/68] DOC-4233 added geospatial examples (#3126) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/geo_tutorial_test.go | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 doctests/geo_tutorial_test.go diff --git a/doctests/geo_tutorial_test.go b/doctests/geo_tutorial_test.go new file mode 100644 index 000000000..051db623b --- /dev/null +++ b/doctests/geo_tutorial_test.go @@ -0,0 +1,139 @@ +// EXAMPLE: geo_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:rentable") + // REMOVE_END + + // STEP_START geoadd + res1, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.27652, + Latitude: 37.805186, + Name: "station:1", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2674626, + Latitude: 37.8062344, + Name: "station:2", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> 1 + + res3, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2469854, + Latitude: 37.8104049, + Name: "station:3", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 1 + // STEP_END + + // Output: + // 1 + // 1 + // 1 +} + +func ExampleClient_geosearch() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "bikes:rentable") + + _, err := rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.27652, + Latitude: 37.805186, + Name: "station:1", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2674626, + Latitude: 37.8062344, + Name: "station:2", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.GeoAdd(ctx, "bikes:rentable", + &redis.GeoLocation{ + Longitude: -122.2469854, + Latitude: 37.8104049, + Name: "station:3", + }).Result() + + if err != nil { + panic(err) + } + // REMOVE_END + + // STEP_START geosearch + res4, err := rdb.GeoSearch(ctx, "bikes:rentable", + &redis.GeoSearchQuery{ + Longitude: -122.27652, + Latitude: 37.805186, + Radius: 5, + RadiusUnit: "km", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> [station:1 station:2 station:3] + // STEP_END + + // Output: + // [station:1 station:2 station:3] +} From 0c84b6231f118c3bc9b039155e1d82007d2a7f55 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:38:32 +0100 Subject: [PATCH 13/68] DOC-4322 added HSET/HGET command page examples (#3140) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_hash_test.go | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 doctests/cmds_hash_test.go diff --git a/doctests/cmds_hash_test.go b/doctests/cmds_hash_test.go new file mode 100644 index 000000000..f9630a9de --- /dev/null +++ b/doctests/cmds_hash_test.go @@ -0,0 +1,133 @@ +// EXAMPLE: cmds_hash +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_hset() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hset + res1, err := rdb.HSet(ctx, "myhash", "field1", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1) // >>> 1 + + res2, err := rdb.HGet(ctx, "myhash", "field1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2) // >>> Hello + + res3, err := rdb.HSet(ctx, "myhash", + "field2", "Hi", + "field3", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3) // >>> 2 + + res4, err := rdb.HGet(ctx, "myhash", "field2").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> Hi + + res5, err := rdb.HGet(ctx, "myhash", "field3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> World + + res6, err := rdb.HGetAll(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6) + // >>> map[field1:Hello field2:Hi field3:World] + // STEP_END + + // Output: + // 1 + // Hello + // 2 + // Hi + // World + // map[field1:Hello field2:Hi field3:World] +} + +func ExampleClient_hget() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hget + res7, err := rdb.HSet(ctx, "myhash", "field1", "foo").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 1 + + res8, err := rdb.HGet(ctx, "myhash", "field1").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> foo + + res9, err := rdb.HGet(ctx, "myhash", "field2").Result() + + if err != nil { + fmt.Println(err) + } + + fmt.Println(res9) // >>> + // STEP_END + + // Output: + // 1 + // foo + // redis: nil +} From f467d014a4590c8e35a4b60f889962833085e0ea Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:40:21 +0100 Subject: [PATCH 14/68] DOC-4323 added INCR command example (#3141) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_string_test.go | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 doctests/cmds_string_test.go diff --git a/doctests/cmds_string_test.go b/doctests/cmds_string_test.go new file mode 100644 index 000000000..fb7801a67 --- /dev/null +++ b/doctests/cmds_string_test.go @@ -0,0 +1,57 @@ +// EXAMPLE: cmds_string +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cmd_incr() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START incr + incrResult1, err := rdb.Set(ctx, "mykey", "10", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult1) // >>> OK + + incrResult2, err := rdb.Incr(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult2) // >>> 11 + + incrResult3, err := rdb.Get(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(incrResult3) // >>> 11 + // STEP_END + + // Output: + // OK + // 11 + // 11 +} From e69895b94d276e03d8e0e88a0e7514060b9a0639 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:42:24 +0100 Subject: [PATCH 15/68] DOC-4324 added ZADD and ZRANGE command examples (#3142) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_sorted_set_test.go | 220 +++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 doctests/cmds_sorted_set_test.go diff --git a/doctests/cmds_sorted_set_test.go b/doctests/cmds_sorted_set_test.go new file mode 100644 index 000000000..8704fc20d --- /dev/null +++ b/doctests/cmds_sorted_set_test.go @@ -0,0 +1,220 @@ +// EXAMPLE: cmds_sorted_set +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_zadd_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zadd + zAddResult1, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult1) // >>> 1 + + zAddResult2, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "uno", Score: 1}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult2) + + zAddResult3, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult3) // >>> 2 + + zAddResult4, err := rdb.ZRangeWithScores(ctx, "myzset", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zAddResult4) // >>> [{1 one} {1 uno} {2 two} {3 three}] + // STEP_END + + // Output: + // 1 + // 1 + // 2 + // [{1 one} {1 uno} {2 two} {3 three}] +} + +func ExampleClient_zrange1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange1 + zrangeResult1, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult1) // >>> 3 + + zrangeResult2, err := rdb.ZRange(ctx, "myzset", 0, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult2) // >>> [one two three] + + zrangeResult3, err := rdb.ZRange(ctx, "myzset", 2, 3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult3) // >>> [three] + + zrangeResult4, err := rdb.ZRange(ctx, "myzset", -2, -1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zrangeResult4) // >>> [two three] + // STEP_END + + // Output: + // 3 + // [one two three] + // [three] + // [two three] +} + +func ExampleClient_zrange2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange2 + zRangeResult5, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult5) // >>> 3 + + zRangeResult6, err := rdb.ZRangeWithScores(ctx, "myzset", 0, 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult6) // >>> [{1 one} {2 two}] + // STEP_END + + // Output: + // 3 + // [{1 one} {2 two}] +} + +func ExampleClient_zrange3() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myzset") + // REMOVE_END + + // STEP_START zrange3 + zRangeResult7, err := rdb.ZAdd(ctx, "myzset", + redis.Z{Member: "one", Score: 1}, + redis.Z{Member: "two", Score: 2}, + redis.Z{Member: "three", Score: 3}, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult7) // >>> 3 + + zRangeResult8, err := rdb.ZRangeArgs(ctx, + redis.ZRangeArgs{ + Key: "myzset", + ByScore: true, + Start: "(1", + Stop: "+inf", + Offset: 1, + Count: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(zRangeResult8) // >>> [three] + // STEP_END + + // Output: + // 3 + // [three] +} From 7d56a2c38d96742796d1fa91b055d15e8cabc343 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:44:29 +0100 Subject: [PATCH 16/68] DOC-4328 added DEL, EXPIRE, and TTL command examples (#3143) Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- doctests/cmds_generic_test.go | 194 ++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 doctests/cmds_generic_test.go diff --git a/doctests/cmds_generic_test.go b/doctests/cmds_generic_test.go new file mode 100644 index 000000000..ab8ebdd53 --- /dev/null +++ b/doctests/cmds_generic_test.go @@ -0,0 +1,194 @@ +// EXAMPLE: cmds_generic +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_del_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "key1", "key2", "key3") + // REMOVE_END + + // STEP_START del + delResult1, err := rdb.Set(ctx, "key1", "Hello", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult1) // >>> OK + + delResult2, err := rdb.Set(ctx, "key2", "World", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult2) // >>> OK + + delResult3, err := rdb.Del(ctx, "key1", "key2", "key3").Result() + + if err != nil { + panic(err) + } + + fmt.Println(delResult3) // >>> 2 + // STEP_END + + // Output: + // OK + // OK + // 2 +} + +func ExampleClient_expire_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START expire + expireResult1, err := rdb.Set(ctx, "mykey", "Hello", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult1) // >>> OK + + expireResult2, err := rdb.Expire(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult2) // >>> true + + expireResult3, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(expireResult3.Seconds())) // >>> 10 + + expireResult4, err := rdb.Set(ctx, "mykey", "Hello World", 0).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult4) // >>> OK + + expireResult5, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult5) // >>> -1ns + + expireResult6, err := rdb.ExpireXX(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult6) // >>> false + + expireResult7, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult7) // >>> -1ns + + expireResult8, err := rdb.ExpireNX(ctx, "mykey", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(expireResult8) // >>> true + + expireResult9, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(expireResult9.Seconds())) // >>> 10 + // STEP_END + + // Output: + // OK + // true + // 10 + // OK + // -1ns + // false + // -1ns + // true + // 10 +} + +func ExampleClient_ttl_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "mykey") + // REMOVE_END + + // STEP_START ttl + ttlResult1, err := rdb.Set(ctx, "mykey", "Hello", 10*time.Second).Result() + + if err != nil { + panic(err) + } + + fmt.Println(ttlResult1) // >>> OK + + ttlResult2, err := rdb.TTL(ctx, "mykey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(math.Round(ttlResult2.Seconds())) // >>> 10 + // STEP_END + + // Output: + // OK + // 10 +} From 20769750085646dd195657bf20edea6f4f6760b9 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:37:22 +0300 Subject: [PATCH 17/68] Support Json with Resp 2 (#3146) * Support ReJSON resp 2 && Test ReJSON against RESP 2 and 3 && Add complex search and json test * Remove comments * Remove unnecessary changes --- json.go | 8 +- json_test.go | 1404 ++++++++++++++++++++++++++++---------------------- 2 files changed, 785 insertions(+), 627 deletions(-) diff --git a/json.go b/json.go index ca731db3a..b3cadf4b7 100644 --- a/json.go +++ b/json.go @@ -60,7 +60,7 @@ type JSONArrTrimArgs struct { type JSONCmd struct { baseCmd val string - expanded []interface{} + expanded interface{} } var _ Cmder = (*JSONCmd)(nil) @@ -100,11 +100,11 @@ func (cmd *JSONCmd) Result() (string, error) { return cmd.Val(), cmd.Err() } -func (cmd JSONCmd) Expanded() (interface{}, error) { +func (cmd *JSONCmd) Expanded() (interface{}, error) { if len(cmd.val) != 0 && cmd.expanded == nil { err := json.Unmarshal([]byte(cmd.val), &cmd.expanded) if err != nil { - return "", err + return nil, err } } @@ -494,7 +494,7 @@ func (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd } // JSONNumIncrBy increments the number value stored at the specified path by the provided number. -// For more information, see https://redis.io/commands/json.numincreby +// For more information, see https://redis.io/docs/latest/commands/json.numincrby/ func (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd { args := []interface{}{"JSON.NUMINCRBY", key, path, value} cmd := newJSONCmd(ctx, args...) diff --git a/json_test.go b/json_test.go index d1ea24290..9139be3ac 100644 --- a/json_test.go +++ b/json_test.go @@ -2,6 +2,8 @@ package redis_test import ( "context" + "encoding/json" + "time" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" @@ -17,644 +19,800 @@ var _ = Describe("JSON Commands", Label("json"), func() { ctx := context.TODO() var client *redis.Client - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - Describe("arrays", Label("arrays"), func() { - It("should JSONArrAppend", Label("json.arrappend", "json"), func() { - cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{2, 3})) + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, }) + } - It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() { - cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]int64{1})) - - cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal("OK")) - - res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(1))) - - res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(-1))) - - res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - stop := 5000 - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(4))) - - stop = -1 - res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res[0]).To(Equal(int64(-1))) - }) - - It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() { - doc := `{ - "store": { - "book": [ - { - "category": "reference", - "author": "Nigel Rees", - "title": "Sayings of the Century", - "price": 8.95, - "size": [10, 20, 30, 40] - }, - { - "category": "fiction", - "author": "Evelyn Waugh", - "title": "Sword of Honour", - "price": 12.99, - "size": [50, 60, 70, 80] - }, - { - "category": "fiction", - "author": "Herman Melville", - "title": "Moby Dick", - "isbn": "0-553-21311-3", - "price": 8.99, - "size": [5, 10, 20, 30] - }, - { - "category": "fiction", - "author": "J. R. R. Tolkien", - "title": "The Lord of the Rings", - "isbn": "0-395-19395-8", - "price": 22.99, - "size": [5, 6, 7, 8] - } - ], - "bicycle": {"color": "red", "price": 19.95} - } - }` - res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]")) - - resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resArr).To(Equal([]int64{1, 2})) - }) - - It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { - cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{6})) - - cmd3 := client.JSONGet(ctx, "insert2") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - // RESP2 vs RESP3 - Expect(cmd3.Val()).To(Or( - Equal(`[100,200,300,1,2,200]`), - Equal(`[[100,200,300,1,2,200]]`))) - }) - - It("should JSONArrLen", Label("json.arrlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrLen(ctx, "length2", "$..a") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]int64{1, 6})) - }) - - It("should JSONArrPop", Label("json.arrpop"), func() { - cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]string{"300"})) - - cmd3 := client.JSONGet(ctx, "pop4", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) - }) - - It("should JSONArrTrim", Label("json.arrtrim", "json"), func() { - cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - stop := 3 - cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]int64{3})) - - res, err := client.JSONGet(ctx, "trim1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[[1,2,3]]`)) - - cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal("OK")) - - stop = 3 - cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd4).To(Equal([]int64{0})) - - cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd5).To(Equal("OK")) - - stop = 99 - cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd6).To(Equal([]int64{2})) - - cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd7).To(Equal("OK")) - - stop = 1 - cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd8).To(Equal([]int64{0})) - - cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd9).To(Equal("OK")) - - stop = 11 - cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd10).To(Equal([]int64{0})) - }) - - It("should JSONArrPop", Label("json.arrpop", "json"), func() { - cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal([]string{"300"})) - - cmd3 := client.JSONGet(ctx, "pop4", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) - }) + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } }) - Describe("get/set", Label("getset"), func() { - It("should JSONSet", Label("json.set", "json"), func() { - cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`) - Expect(cmd.Err()).NotTo(HaveOccurred()) - Expect(cmd.Val()).To(Equal("OK")) + protocols := []int{2, 3} + for _, protocol := range protocols { + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) }) - It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() { - res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`{-"a":1,-"b":2}`)) - - res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`)) + Describe("arrays", Label("arrays"), func() { + It("should JSONArrAppend", Label("json.arrappend", "json"), func() { + cmd1 := client.JSONSet(ctx, "append2", "$", `{"a": [10], "b": {"a": [12, 13]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrAppend(ctx, "append2", "$..a", 10) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{2, 3})) + }) + + It("should JSONArrIndex and JSONArrIndexWithArgs", Label("json.arrindex", "json"), func() { + cmd1, err := client.JSONSet(ctx, "index1", "$", `{"a": [10], "b": {"a": [12, 10]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + cmd2, err := client.JSONArrIndex(ctx, "index1", "$.b.a", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]int64{1})) + + cmd3, err := client.JSONSet(ctx, "index2", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal("OK")) + + res, err := client.JSONArrIndex(ctx, "index2", "$", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(1))) + + res, err = client.JSONArrIndex(ctx, "index2", "$", 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(-1))) + + res, err = client.JSONArrIndex(ctx, "index2", "$", 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + stop := 5000 + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(4))) + + stop = -1 + res, err = client.JSONArrIndexWithArgs(ctx, "index2", "$", &redis.JSONArrIndexArgs{Stop: &stop}, 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(Equal(int64(-1))) + }) + + It("should JSONArrIndex and JSONArrIndexWithArgs with $", Label("json.arrindex", "json"), func() { + doc := `{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + "size": [10, 20, 30, 40] + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + "size": [50, 60, 70, 80] + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + "size": [5, 10, 20, 30] + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "size": [5, 6, 7, 8] + } + ], + "bicycle": {"color": "red", "price": 19.95} + } + }` + res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + resGet, err := client.JSONGet(ctx, "doc1", "$.store.book[?(@.price<10)].size").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal("[[10,20,30,40],[5,10,20,30]]")) + + resArr, err := client.JSONArrIndex(ctx, "doc1", "$.store.book[?(@.price<10)].size", 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resArr).To(Equal([]int64{1, 2})) + }) + + It("should JSONArrInsert", Label("json.arrinsert", "json"), func() { + cmd1 := client.JSONSet(ctx, "insert2", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrInsert(ctx, "insert2", "$", -1, 1, 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{6})) + + cmd3 := client.JSONGet(ctx, "insert2") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + // RESP2 vs RESP3 + Expect(cmd3.Val()).To(Or( + Equal(`[100,200,300,1,2,200]`), + Equal(`[[100,200,300,1,2,200]]`))) + }) + + It("should JSONArrLen", Label("json.arrlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "length2", "$", `{"a": [10], "b": {"a": [12, 10, 20, 12, 90, 10]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrLen(ctx, "length2", "$..a") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]int64{1, 6})) + }) + + It("should JSONArrPop", Label("json.arrpop"), func() { + cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]string{"300"})) + + cmd3 := client.JSONGet(ctx, "pop4", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) + }) + + It("should JSONArrTrim", Label("json.arrtrim", "json"), func() { + cmd1, err := client.JSONSet(ctx, "trim1", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + stop := 3 + cmd2, err := client.JSONArrTrimWithArgs(ctx, "trim1", "$", &redis.JSONArrTrimArgs{Start: 1, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]int64{3})) + + res, err := client.JSONGet(ctx, "trim1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[[1,2,3]]`)) + + cmd3, err := client.JSONSet(ctx, "trim2", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal("OK")) + + stop = 3 + cmd4, err := client.JSONArrTrimWithArgs(ctx, "trim2", "$", &redis.JSONArrTrimArgs{Start: -1, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd4).To(Equal([]int64{0})) + + cmd5, err := client.JSONSet(ctx, "trim3", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd5).To(Equal("OK")) + + stop = 99 + cmd6, err := client.JSONArrTrimWithArgs(ctx, "trim3", "$", &redis.JSONArrTrimArgs{Start: 3, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd6).To(Equal([]int64{2})) + + cmd7, err := client.JSONSet(ctx, "trim4", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd7).To(Equal("OK")) + + stop = 1 + cmd8, err := client.JSONArrTrimWithArgs(ctx, "trim4", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd8).To(Equal([]int64{0})) + + cmd9, err := client.JSONSet(ctx, "trim5", "$", `[0,1,2,3,4]`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd9).To(Equal("OK")) + + stop = 11 + cmd10, err := client.JSONArrTrimWithArgs(ctx, "trim5", "$", &redis.JSONArrTrimArgs{Start: 9, Stop: &stop}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd10).To(Equal([]int64{0})) + }) + + It("should JSONArrPop", Label("json.arrpop", "json"), func() { + cmd1 := client.JSONSet(ctx, "pop4", "$", `[100, 200, 300, 200]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONArrPop(ctx, "pop4", "$", 2) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal([]string{"300"})) + + cmd3 := client.JSONGet(ctx, "pop4", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal("[[100,200,200]]")) + }) }) - It("should JSONMerge", Label("json.merge", "json"), func() { - res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONGet(ctx, "merge1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`)) + Describe("get/set", Label("getset"), func() { + It("should JSONSet", Label("json.set", "json"), func() { + cmd := client.JSONSet(ctx, "set1", "$", `{"a": 1, "b": 2, "hello": "world"}`) + Expect(cmd.Err()).NotTo(HaveOccurred()) + Expect(cmd.Val()).To(Equal("OK")) + }) + + It("should JSONGet", Label("json.get", "json", "NonRedisEnterprise"), func() { + res, err := client.JSONSet(ctx, "get3", "$", `{"a": 1, "b": 2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`{-"a":1,-"b":2}`)) + + res, err = client.JSONGetWithArgs(ctx, "get3", &redis.JSONGetArgs{Indent: "-", Newline: `~`, Space: `!`}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`{~-"a":!1,~-"b":!2~}`)) + }) + + It("should JSONMerge", Label("json.merge", "json"), func() { + res, err := client.JSONSet(ctx, "merge1", "$", `{"a": 1, "b": 2}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONMerge(ctx, "merge1", "$", `{"b": 3, "c": 4}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONGet(ctx, "merge1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[{"a":1,"b":3,"c":4}]`)) + }) + + It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() { + doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`} + doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2} + docs := []redis.JSONSetArgs{doc1, doc2} + + mSetResult, err := client.JSONMSetArgs(ctx, docs).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(mSetResult).To(Equal("OK")) + + res, err := client.JSONMGet(ctx, "$", "mset1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]interface{}{`[{"a":1}]`})) + + res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"})) + + _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() { + cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal("OK")) + + cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(HaveLen(2)) + Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`)) + Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`)) + }) + + It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{`[1,3,""]`})) + + iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`})) + + iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal([]interface{}{nil, nil})) + }) }) - It("should JSONMSet", Label("json.mset", "json", "NonRedisEnterprise"), func() { - doc1 := redis.JSONSetArgs{Key: "mset1", Path: "$", Value: `{"a": 1}`} - doc2 := redis.JSONSetArgs{Key: "mset2", Path: "$", Value: 2} - docs := []redis.JSONSetArgs{doc1, doc2} - - mSetResult, err := client.JSONMSetArgs(ctx, docs).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(mSetResult).To(Equal("OK")) - - res, err := client.JSONMGet(ctx, "$", "mset1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal([]interface{}{`[{"a":1}]`})) - - res, err = client.JSONMGet(ctx, "$", "mset1", "mset2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal([]interface{}{`[{"a":1}]`, "[2]"})) - - _, err = client.JSONMSet(ctx, "mset1", "$.a", 2, "mset3", "$", `[1]`).Result() - Expect(err).NotTo(HaveOccurred()) + Describe("Misc", Label("misc"), func() { + It("should JSONClear", Label("json.clear", "json"), func() { + cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONClear(ctx, "clear1", "$") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(1))) + + cmd3 := client.JSONGet(ctx, "clear1", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal(`[[]]`)) + }) + + It("should JSONClear with $", Label("json.clear", "json"), func() { + doc := `{ + "nested1": {"a": {"foo": 10, "bar": 20}}, + "a": ["foo"], + "nested2": {"a": "claro"}, + "nested3": {"a": {"baz": 50}} + }` + res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resGet, err := client.JSONGet(ctx, "doc1", `$`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`)) + + res, err = client.JSONSet(ctx, "doc1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "doc1", `$`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`)) + }) + + It("should JSONDel", Label("json.del", "json"), func() { + cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONDel(ctx, "del1", "$") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(1))) + + cmd3 := client.JSONGet(ctx, "del1", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(HaveLen(0)) + }) + + It("should JSONDel with $", Label("json.del", "json"), func() { + res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONDel(ctx, "del1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(2))) + + resGet, err := client.JSONGet(ctx, "del1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) + + res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONDel(ctx, "del2", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "del2", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) + + doc := `[ + { + "ciao": ["non ancora"], + "nested": [ + {"ciao": [1, "a"]}, + {"ciao": [2, "a"]}, + {"ciaoc": [3, "non", "ciao"]}, + {"ciao": [4, "a"]}, + {"e": [5, "non", "ciao"]} + ] + } + ]` + res, err = client.JSONSet(ctx, "del3", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` + resGet, err = client.JSONGet(ctx, "del3", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(resVal)) + }) + + It("should JSONForget", Label("json.forget", "json"), func() { + cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONForget(ctx, "forget3", "$..a") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(int64(2))) + + cmd3 := client.JSONGet(ctx, "forget3", "$") + Expect(cmd3.Err()).NotTo(HaveOccurred()) + Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`)) + }) + + It("should JSONForget with $", Label("json.forget", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(2))) + + resGet, err := client.JSONGet(ctx, "doc1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(1))) + + resGet, err = client.JSONGet(ctx, "doc2", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) + + doc := `[ + { + "ciao": ["non ancora"], + "nested": [ + {"ciao": [1, "a"]}, + {"ciao": [2, "a"]}, + {"ciaoc": [3, "non", "ciao"]}, + {"ciao": [4, "a"]}, + {"e": [5, "non", "ciao"]} + ] + } + ]` + res, err = client.JSONSet(ctx, "doc3", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(iRes).To(Equal(int64(3))) + + resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` + resGet, err = client.JSONGet(ctx, "doc3", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(Equal(resVal)) + }) + + It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() { + cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1)) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(Equal(`[3,0]`)) + }) + + It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[7]`)) + + res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[10.5]`)) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(`[5]`)) + }) + + It("should JSONObjKeys", Label("json.objkeys", "json"), func() { + cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(7)) + Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil})) + }) + + It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() { + doc := `{ + "nested1": {"a": {"foo": 10, "bar": 20}}, + "a": ["foo"], + "nested2": {"a": {"baz": 50}} + }` + cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + + cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}})) + + cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{"foo", "bar"})) + + cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd2).To(Equal([]interface{}{"baz"})) + + _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should JSONObjLen", Label("json.objlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(7)) + Expect(cmd2.Val()[0]).To(BeNil()) + Expect(*cmd2.Val()[1]).To(Equal(int64(1))) + }) + + It("should JSONStrLen", Label("json.strlen", "json"), func() { + cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(5)) + var tmp int64 = 20 + Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp)) + Expect(*cmd2.Val()[0]).To(Equal(int64(5))) + Expect(*cmd2.Val()[1]).To(Equal(int64(3))) + Expect(cmd2.Val()[2]).To(BeNil()) + Expect(*cmd2.Val()[3]).To(Equal(int64(5))) + Expect(*cmd2.Val()[4]).To(Equal(int64(3))) + }) + + It("should JSONStrAppend", Label("json.strappend", "json"), func() { + cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd1).To(Equal("OK")) + cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*cmd2[0]).To(Equal(int64(6))) + cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cmd3).To(Equal(`["foobar"]`)) + }) + + It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() { + res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*intArrayResult[0]).To(Equal(int64(8))) + + res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal("OK")) + + intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*intResult[0]).To(Equal(int64(5))) + }) + + It("should JSONToggle", Label("json.toggle", "json"), func() { + cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(1)) + Expect(*cmd2.Val()[0]).To(Equal(int64(0))) + }) + + It("should JSONType", Label("json.type", "json"), func() { + cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`) + Expect(cmd1.Err()).NotTo(HaveOccurred()) + Expect(cmd1.Val()).To(Equal("OK")) + + cmd2 := client.JSONType(ctx, "type1", "$[0]") + Expect(cmd2.Err()).NotTo(HaveOccurred()) + Expect(cmd2.Val()).To(HaveLen(1)) + // RESP2 v RESP3 + Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) + }) }) + } +}) - It("should JSONMGet", Label("json.mget", "json", "NonRedisEnterprise"), func() { - cmd1 := client.JSONSet(ctx, "mget2a", "$", `{"a": ["aa", "ab", "ac", "ad"], "b": {"a": ["ba", "bb", "bc", "bd"]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - cmd2 := client.JSONSet(ctx, "mget2b", "$", `{"a": [100, 200, 300, 200], "b": {"a": [100, 200, 300, 200]}}`) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal("OK")) - - cmd3 := client.JSONMGet(ctx, "$..a", "mget2a", "mget2b") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(HaveLen(2)) - Expect(cmd3.Val()[0]).To(Equal(`[["aa","ab","ac","ad"],["ba","bb","bc","bd"]]`)) - Expect(cmd3.Val()[1]).To(Equal(`[[100,200,300,200],[100,200,300,200]]`)) +var _ = Describe("Go-Redis Advanced JSON and RediSearch Tests", func() { + var client *redis.Client + var ctx = context.Background() + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, // Setting RESP2 or RESP3 protocol + UnstableResp3: true, // Enable RESP3 features }) + } - It("should JSONMget with $", Label("json.mget", "json", "NonRedisEnterprise"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "b": 2, "nested": {"a": 3}, "c": "", "nested2": {"a": ""}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": 4, "b": 5, "nested": {"a": 6}, "c": "", "nested2": {"a": [""]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONMGet(ctx, "$..a", "doc1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{`[1,3,""]`})) - - iRes, err = client.JSONMGet(ctx, "$..a", "doc1", "doc2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{`[1,3,""]`, `[4,6,[""]]`})) - - iRes, err = client.JSONMGet(ctx, "$..a", "non_existing_doc", "non_existing_doc1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal([]interface{}{nil, nil})) - }) + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } }) - Describe("Misc", Label("misc"), func() { - It("should JSONClear", Label("json.clear", "json"), func() { - cmd1 := client.JSONSet(ctx, "clear1", "$", `[1]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONClear(ctx, "clear1", "$") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(1))) - - cmd3 := client.JSONGet(ctx, "clear1", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal(`[[]]`)) - }) - - It("should JSONClear with $", Label("json.clear", "json"), func() { - doc := `{ - "nested1": {"a": {"foo": 10, "bar": 20}}, - "a": ["foo"], - "nested2": {"a": "claro"}, - "nested3": {"a": {"baz": 50}} - }` - res, err := client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONClear(ctx, "doc1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resGet, err := client.JSONGet(ctx, "doc1", `$`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":[],"nested2":{"a":"claro"},"nested3":{"a":{}}}]`)) - - res, err = client.JSONSet(ctx, "doc1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONClear(ctx, "doc1", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "doc1", `$`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested1":{"a":{}},"a":["foo"],"nested2":{"a":"claro"},"nested3":{"a":{"baz":50}}}]`)) - }) - - It("should JSONDel", Label("json.del", "json"), func() { - cmd1 := client.JSONSet(ctx, "del1", "$", `[1]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONDel(ctx, "del1", "$") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(1))) - - cmd3 := client.JSONGet(ctx, "del1", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(HaveLen(0)) - }) - - It("should JSONDel with $", Label("json.del", "json"), func() { - res, err := client.JSONSet(ctx, "del1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONDel(ctx, "del1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(2))) - - resGet, err := client.JSONGet(ctx, "del1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) - - res, err = client.JSONSet(ctx, "del2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONDel(ctx, "del2", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "del2", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) - - doc := `[ - { - "ciao": ["non ancora"], - "nested": [ - {"ciao": [1, "a"]}, - {"ciao": [2, "a"]}, - {"ciaoc": [3, "non", "ciao"]}, - {"ciao": [4, "a"]}, - {"e": [5, "non", "ciao"]} - ] - } - ]` - res, err = client.JSONSet(ctx, "del3", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONDel(ctx, "del3", `$.[0]["nested"]..ciao`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` - resGet, err = client.JSONGet(ctx, "del3", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(resVal)) - }) - - It("should JSONForget", Label("json.forget", "json"), func() { - cmd1 := client.JSONSet(ctx, "forget3", "$", `{"a": [1,2,3], "b": {"a": [1,2,3], "b": "annie"}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONForget(ctx, "forget3", "$..a") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(int64(2))) - - cmd3 := client.JSONGet(ctx, "forget3", "$") - Expect(cmd3.Err()).NotTo(HaveOccurred()) - Expect(cmd3.Val()).To(Equal(`[{"b":{"b":"annie"}}]`)) - }) - - It("should JSONForget with $", Label("json.forget", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": 1, "nested": {"a": 2, "b": 3}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err := client.JSONForget(ctx, "doc1", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(2))) - - resGet, err := client.JSONGet(ctx, "doc1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":3}}]`)) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": {"a": 2, "b": 3}, "b": ["a", "b"], "nested": {"b": [true, "a", "b"]}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONForget(ctx, "doc2", "$..a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(1))) - - resGet, err = client.JSONGet(ctx, "doc2", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(`[{"nested":{"b":[true,"a","b"]},"b":["a","b"]}]`)) - - doc := `[ - { - "ciao": ["non ancora"], - "nested": [ - {"ciao": [1, "a"]}, - {"ciao": [2, "a"]}, - {"ciaoc": [3, "non", "ciao"]}, - {"ciao": [4, "a"]}, - {"e": [5, "non", "ciao"]} - ] - } - ]` - res, err = client.JSONSet(ctx, "doc3", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - iRes, err = client.JSONForget(ctx, "doc3", `$.[0]["nested"]..ciao`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(iRes).To(Equal(int64(3))) - - resVal := `[[{"ciao":["non ancora"],"nested":[{},{},{"ciaoc":[3,"non","ciao"]},{},{"e":[5,"non","ciao"]}]}]]` - resGet, err = client.JSONGet(ctx, "doc3", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resGet).To(Equal(resVal)) - }) - - It("should JSONNumIncrBy", Label("json.numincrby", "json"), func() { - cmd1 := client.JSONSet(ctx, "incr3", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONNumIncrBy(ctx, "incr3", "$..a[1]", float64(1)) - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(Equal(`[3,0]`)) - }) - - It("should JSONNumIncrBy with $", Label("json.numincrby", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[7]`)) - - res, err = client.JSONNumIncrBy(ctx, "doc1", "$.b[1].a", 3.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[10.5]`)) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "b", "b": [{"a": 2}, {"a": 5.0}, {"a": "c"}]}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - res, err = client.JSONNumIncrBy(ctx, "doc2", "$.b[0].a", 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(`[5]`)) - }) - - It("should JSONObjKeys", Label("json.objkeys", "json"), func() { - cmd1 := client.JSONSet(ctx, "objkeys1", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONObjKeys(ctx, "objkeys1", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(7)) - Expect(cmd2.Val()).To(Equal([]interface{}{nil, []interface{}{"a"}, nil, nil, nil, nil, nil})) - }) - - It("should JSONObjKeys with $", Label("json.objkeys", "json"), func() { - doc := `{ - "nested1": {"a": {"foo": 10, "bar": 20}}, - "a": ["foo"], - "nested2": {"a": {"baz": 50}} - }` - cmd1, err := client.JSONSet(ctx, "objkeys1", "$", doc).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - - cmd2, err := client.JSONObjKeys(ctx, "objkeys1", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{[]interface{}{"foo", "bar"}})) - - cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".*.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{"foo", "bar"})) - - cmd2, err = client.JSONObjKeys(ctx, "objkeys1", ".nested2.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd2).To(Equal([]interface{}{"baz"})) - - _, err = client.JSONObjKeys(ctx, "non_existing_doc", "..a").Result() - Expect(err).To(HaveOccurred()) - }) - - It("should JSONObjLen", Label("json.objlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "objlen2", "$", `{"a": [1, 2], "b": {"a": [0, -1]}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONObjLen(ctx, "objlen2", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(7)) - Expect(cmd2.Val()[0]).To(BeNil()) - Expect(*cmd2.Val()[1]).To(Equal(int64(1))) - }) - - It("should JSONStrLen", Label("json.strlen", "json"), func() { - cmd1 := client.JSONSet(ctx, "strlen2", "$", `{"a": "alice", "b": "bob", "c": {"a": "alice", "b": "bob"}}`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONStrLen(ctx, "strlen2", "$..*") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(5)) - var tmp int64 = 20 - Expect(cmd2.Val()[0]).To(BeAssignableToTypeOf(&tmp)) - Expect(*cmd2.Val()[0]).To(Equal(int64(5))) - Expect(*cmd2.Val()[1]).To(Equal(int64(3))) - Expect(cmd2.Val()[2]).To(BeNil()) - Expect(*cmd2.Val()[3]).To(Equal(int64(5))) - Expect(*cmd2.Val()[4]).To(Equal(int64(3))) - }) - - It("should JSONStrAppend", Label("json.strappend", "json"), func() { - cmd1, err := client.JSONSet(ctx, "strapp1", "$", `"foo"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd1).To(Equal("OK")) - cmd2, err := client.JSONStrAppend(ctx, "strapp1", "$", `"bar"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*cmd2[0]).To(Equal(int64(6))) - cmd3, err := client.JSONGet(ctx, "strapp1", "$").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cmd3).To(Equal(`["foobar"]`)) - }) - - It("should JSONStrAppend and JSONStrLen with $", Label("json.strappend", "json.strlen", "json"), func() { - res, err := client.JSONSet(ctx, "doc1", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - intArrayResult, err := client.JSONStrAppend(ctx, "doc1", "$.nested1.a", `"baz"`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*intArrayResult[0]).To(Equal(int64(8))) - - res, err = client.JSONSet(ctx, "doc2", "$", `{"a": "foo", "nested1": {"a": "hello"}, "nested2": {"a": 31}}`).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal("OK")) - - intResult, err := client.JSONStrLen(ctx, "doc2", "$.nested1.a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(*intResult[0]).To(Equal(int64(5))) - }) - - It("should JSONToggle", Label("json.toggle", "json"), func() { - cmd1 := client.JSONSet(ctx, "toggle1", "$", `[true]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONToggle(ctx, "toggle1", "$[0]") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(1)) - Expect(*cmd2.Val()[0]).To(Equal(int64(0))) - }) - - It("should JSONType", Label("json.type", "json"), func() { - cmd1 := client.JSONSet(ctx, "type1", "$", `[true]`) - Expect(cmd1.Err()).NotTo(HaveOccurred()) - Expect(cmd1.Val()).To(Equal("OK")) - - cmd2 := client.JSONType(ctx, "type1", "$[0]") - Expect(cmd2.Err()).NotTo(HaveOccurred()) - Expect(cmd2.Val()).To(HaveLen(1)) - // RESP2 v RESP3 - Expect(cmd2.Val()[0]).To(Or(Equal([]interface{}{"boolean"}), Equal("boolean"))) - }) + Context("when testing with RESP2 and RESP3", func() { + protocols := []int{2, 3} + + for _, protocol := range protocols { + When("using protocol version", func() { + BeforeEach(func() { + client = setupRedisClient(protocol) + }) + + It("should perform complex JSON and RediSearch operations", func() { + jsonDoc := map[string]interface{}{ + "person": map[string]interface{}{ + "name": "Alice", + "age": 30, + "status": true, + "address": map[string]interface{}{ + "city": "Wonderland", + "postcode": "12345", + }, + "contacts": []map[string]interface{}{ + {"type": "email", "value": "alice@example.com"}, + {"type": "phone", "value": "+123456789"}, + {"type": "fax", "value": "+987654321"}, + }, + "friends": []map[string]interface{}{ + {"name": "Bob", "age": 35, "status": true}, + {"name": "Charlie", "age": 28, "status": false}, + }, + }, + "settings": map[string]interface{}{ + "notifications": map[string]interface{}{ + "email": true, + "sms": false, + "alerts": []string{"low battery", "door open"}, + }, + "theme": "dark", + }, + } + + setCmd := client.JSONSet(ctx, "person:1", ".", jsonDoc) + Expect(setCmd.Err()).NotTo(HaveOccurred(), "JSON.SET failed") + + getCmdRaw := client.JSONGet(ctx, "person:1", ".") + rawJSON, err := getCmdRaw.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.GET (raw) failed") + GinkgoWriter.Printf("Raw JSON: %s\n", rawJSON) + + getCmdExpanded := client.JSONGet(ctx, "person:1", ".") + expandedJSON, err := getCmdExpanded.Expanded() + Expect(err).NotTo(HaveOccurred(), "JSON.GET (expanded) failed") + GinkgoWriter.Printf("Expanded JSON: %+v\n", expandedJSON) + + Expect(rawJSON).To(MatchJSON(jsonMustMarshal(expandedJSON))) + + arrAppendCmd := client.JSONArrAppend(ctx, "person:1", "$.person.contacts", `{"type": "social", "value": "@alice_wonder"}`) + Expect(arrAppendCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRAPPEND failed") + arrLenCmd := client.JSONArrLen(ctx, "person:1", "$.person.contacts") + arrLen, err := arrLenCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.ARRLEN failed") + Expect(arrLen).To(Equal([]int64{4}), "Array length mismatch after append") + + arrInsertCmd := client.JSONArrInsert(ctx, "person:1", "$.person.friends", 1, `{"name": "Diana", "age": 25, "status": true}`) + Expect(arrInsertCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRINSERT failed") + + start := 0 + stop := 1 + arrTrimCmd := client.JSONArrTrimWithArgs(ctx, "person:1", "$.person.friends", &redis.JSONArrTrimArgs{Start: start, Stop: &stop}) + Expect(arrTrimCmd.Err()).NotTo(HaveOccurred(), "JSON.ARRTRIM failed") + + mergeData := map[string]interface{}{ + "status": false, + "nickname": "WonderAlice", + "lastLogin": time.Now().Format(time.RFC3339), + } + mergeCmd := client.JSONMerge(ctx, "person:1", "$.person", jsonMustMarshal(mergeData)) + Expect(mergeCmd.Err()).NotTo(HaveOccurred(), "JSON.MERGE failed") + + typeCmd := client.JSONType(ctx, "person:1", "$.person.nickname") + nicknameType, err := typeCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.TYPE failed") + Expect(nicknameType[0]).To(Equal([]interface{}{"string"}), "JSON.TYPE mismatch for nickname") + + createIndexCmd := client.Do(ctx, "FT.CREATE", "person_idx", "ON", "JSON", + "PREFIX", "1", "person:", "SCHEMA", + "$.person.name", "AS", "name", "TEXT", + "$.person.age", "AS", "age", "NUMERIC", + "$.person.address.city", "AS", "city", "TEXT", + "$.person.contacts[*].value", "AS", "contact_value", "TEXT", + ) + Expect(createIndexCmd.Err()).NotTo(HaveOccurred(), "FT.CREATE failed") + + searchCmd := client.FTSearchWithArgs(ctx, "person_idx", "@contact_value:(alice\\@example\\.com alice_wonder)", &redis.FTSearchOptions{Return: []redis.FTSearchReturn{{FieldName: "$.person.name"}, {FieldName: "$.person.age"}, {FieldName: "$.person.address.city"}}}) + searchResult, err := searchCmd.Result() + Expect(err).NotTo(HaveOccurred(), "FT.SEARCH failed") + GinkgoWriter.Printf("Advanced Search result: %+v\n", searchResult) + + incrCmd := client.JSONNumIncrBy(ctx, "person:1", "$.person.age", 5) + incrResult, err := incrCmd.Result() + Expect(err).NotTo(HaveOccurred(), "JSON.NUMINCRBY failed") + Expect(incrResult).To(Equal("[35]"), "Age increment mismatch") + + delCmd := client.JSONDel(ctx, "person:1", "$.settings.notifications.email") + Expect(delCmd.Err()).NotTo(HaveOccurred(), "JSON.DEL failed") + + typeCmd = client.JSONType(ctx, "person:1", "$.settings.notifications.email") + typeResult, err := typeCmd.Result() + Expect(err).ToNot(HaveOccurred()) + Expect(typeResult[0]).To(BeEmpty(), "Expected JSON.TYPE to be empty for deleted field") + }) + }) + } }) }) + +// Helper function to marshal data into JSON for comparisons +func jsonMustMarshal(v interface{}) string { + bytes, err := json.Marshal(v) + Expect(err).NotTo(HaveOccurred()) + return string(bytes) +} From a8590e987945b7ba050569cc3b94b8ece49e99e3 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:52:50 +0100 Subject: [PATCH 18/68] Fix field name spellings (#3132) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- search_commands.go | 24 ++++++++++++------------ search_test.go | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/search_commands.go b/search_commands.go index 1a8a4cfef..e4df0b6fc 100644 --- a/search_commands.go +++ b/search_commands.go @@ -16,7 +16,7 @@ type SearchCmdable interface { FTAliasAdd(ctx context.Context, index string, alias string) *StatusCmd FTAliasDel(ctx context.Context, alias string) *StatusCmd FTAliasUpdate(ctx context.Context, index string, alias string) *StatusCmd - FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd + FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd @@ -57,7 +57,7 @@ type FTCreateOptions struct { NoFields bool NoFreqs bool StopWords []interface{} - SkipInitalScan bool + SkipInitialScan bool } type FieldSchema struct { @@ -70,7 +70,7 @@ type FieldSchema struct { NoIndex bool PhoneticMatcher string Weight float64 - Seperator string + Separator string CaseSensitive bool WithSuffixtrie bool VectorArgs *FTVectorArgs @@ -285,7 +285,7 @@ type FTSearchSortBy struct { type FTSearchOptions struct { NoContent bool Verbatim bool - NoStopWrods bool + NoStopWords bool WithScores bool WithPayloads bool WithSortKeys bool @@ -808,13 +808,13 @@ func (c cmdable) FTAliasUpdate(ctx context.Context, index string, alias string) } // FTAlter - Alters the definition of an existing index. -// The 'index' parameter specifies the index to alter, and the 'skipInitalScan' parameter specifies whether to skip the initial scan. +// The 'index' parameter specifies the index to alter, and the 'skipInitialScan' parameter specifies whether to skip the initial scan. // The 'definition' parameter specifies the new definition for the index. // For more information, please refer to the Redis documentation: // [FT.ALTER]: (https://redis.io/commands/ft.alter/) -func (c cmdable) FTAlter(ctx context.Context, index string, skipInitalScan bool, definition []interface{}) *StatusCmd { +func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []interface{}) *StatusCmd { args := []interface{}{"FT.ALTER", index} - if skipInitalScan { + if skipInitialScan { args = append(args, "SKIPINITIALSCAN") } args = append(args, "SCHEMA", "ADD") @@ -907,7 +907,7 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp args = append(args, "STOPWORDS", len(options.StopWords)) args = append(args, options.StopWords...) } - if options.SkipInitalScan { + if options.SkipInitialScan { args = append(args, "SKIPINITIALSCAN") } } @@ -1003,8 +1003,8 @@ func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOp if schema.Weight > 0 { args = append(args, "WEIGHT", schema.Weight) } - if schema.Seperator != "" { - args = append(args, "SEPERATOR", schema.Seperator) + if schema.Separator != "" { + args = append(args, "SEPARATOR", schema.Separator) } if schema.CaseSensitive { args = append(args, "CASESENSITIVE") @@ -1694,7 +1694,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } - if options.NoStopWrods { + if options.NoStopWords { queryArgs = append(queryArgs, "NOSTOPWORDS") } if options.WithScores { @@ -1808,7 +1808,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin if options.Verbatim { args = append(args, "VERBATIM") } - if options.NoStopWrods { + if options.NoStopWords { args = append(args, "NOSTOPWORDS") } if options.WithScores { diff --git a/search_test.go b/search_test.go index efdc6bb1e..48b9aa39b 100644 --- a/search_test.go +++ b/search_test.go @@ -637,11 +637,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) - It("should FTSearch SkipInitalScan", Label("search", "ftsearch"), func() { + It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() { client.HSet(ctx, "doc1", "foo", "bar") text1 := &redis.FieldSchema{FieldName: "foo", FieldType: redis.SearchFieldTypeText} - val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitalScan: true}, text1).Result() + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{SkipInitialScan: true}, text1).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "idx1") From cc9bcb0c0fd764bf7518aa18815e5bad27f14384 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:32:53 +0300 Subject: [PATCH 19/68] Updated package version (#3158) --- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- version.go | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 715454c65..d725db0bb 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index f68ff25d5..7093be420 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 176e03d09..85a82860a 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 2b5030ab6..4d97da4d1 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index d8e9bfffe..3825432a7 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.1 +require github.com/redis/go-redis/v9 v9.6.2 require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 45423ec52..fca1a5972 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 33221d208..bae3f7b93 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 7bc65f9ed..594cfdf1e 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 3a95b56e9..b2e30b394 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.6.2 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 342836007..5cbafac11 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.6.1 + github.com/redis/go-redis/v9 v9.6.2 ) require ( diff --git a/version.go b/version.go index b1234dac3..7cb060b5d 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.6.1" + return "9.6.2" } From 1ed936eb097d10f3aaf40a44d282778f879eac32 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:47:25 +0100 Subject: [PATCH 20/68] DOC-4232 stream code examples (#3128) * DOC-4232 added first stream example * DOC-4232 examples up to xadd_7 * DOC-4232 examples up to xread * DOC-4232 examples up to xclaim * DOC-4232 added remaining examples * DOC-4232 more fixes * DOC-4232 fix for test fail on CI build --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/stream_tutorial_test.go | 1073 ++++++++++++++++++++++++++++++ 1 file changed, 1073 insertions(+) create mode 100644 doctests/stream_tutorial_test.go diff --git a/doctests/stream_tutorial_test.go b/doctests/stream_tutorial_test.go new file mode 100644 index 000000000..093324705 --- /dev/null +++ b/doctests/stream_tutorial_test.go @@ -0,0 +1,1073 @@ +// EXAMPLE: stream_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +// REMOVE_START +func UNUSED(v ...interface{}) {} + +// REMOVE_END + +func ExampleClient_xadd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + // STEP_START xadd + res1, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res1) // >>> 1692632086370-0 + + res2, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.PrintLn(res2) // >>> 1692632094485-0 + + res3, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res3) // >>> 1692632102976-0 + // STEP_END + + // REMOVE_START + UNUSED(res1, res2, res3) + // REMOVE_END + + xlen, err := rdb.XLen(ctx, "race:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(xlen) // >>> 3 + + // Output: + // 3 +} + +func ExampleClient_racefrance1() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + ID: "1692632094485-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + ID: "1692632102976-0", + }).Result() + + if err != nil { + panic(err) + } + + // STEP_START xrange + res4, err := rdb.XRangeN(ctx, "race:france", "1691765278160-0", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... + // STEP_END + + // STEP_START xread_block + res5, err := rdb.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"race:france", "0"}, + Count: 100, + Block: 300, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) + // >>> // [{race:france [{1692632086370-0 map[location_id:1 position:1... + // STEP_END + + // STEP_START xadd_2 + res6, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 29.9, + "position": 1, + "location_id": 2, + }, + }).Result() + + if err != nil { + panic(err) + } + + //fmt.Println(res6) // >>> 1692632147973-0 + // STEP_END + + // STEP_START xlen + res7, err := rdb.XLen(ctx, "race:france").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7) // >>> 4 + // STEP_END + + // REMOVE_START + UNUSED(res6) + // REMOVE_END + + // Output: + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]}]}] + // 4 +} + +func ExampleClient_raceusa() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:usa") + // REMOVE_END + + // STEP_START xadd_id + res8, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Castilla", + }, + ID: "0-1", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res8) // >>> 0-1 + + res9, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Norem", + }, + ID: "0-2", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res9) // >>> 0-2 + // STEP_END + + // STEP_START xadd_bad_id + res10, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Values: map[string]interface{}{ + "racer": "Prickett", + }, + ID: "0-1", + }).Result() + + if err != nil { + // fmt.Println(err) + // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item + } + // STEP_END + + // STEP_START xadd_7 + res11, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:usa", + Values: map[string]interface{}{ + "racer": "Prickett", + }, + ID: "0-*", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res11) // >>> 0-3 + // STEP_END + + // REMOVE_START + UNUSED(res10) + // REMOVE_END + + // Output: + // 0-1 + // 0-2 + // 0-3 +} + +func ExampleClient_racefrance2() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Norem", + "speed": 28.8, + "position": 3, + "location_id": 1, + }, + ID: "1692632094485-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Prickett", + "speed": 29.7, + "position": 2, + "location_id": 1, + }, + ID: "1692632102976-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 29.9, + "position": 1, + "location_id": 2, + }, + ID: "1692632147973-0", + }).Result() + + if err != nil { + panic(err) + } + // STEP_START xrange_all + res12, err := rdb.XRange(ctx, "race:france", "-", "+").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res12) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... + // STEP_END + + // STEP_START xrange_time + res13, err := rdb.XRange(ctx, "race:france", + "1692632086369", "1692632086371", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res13) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]}] + // STEP_END + + // STEP_START xrange_step_1 + res14, err := rdb.XRangeN(ctx, "race:france", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res14) + // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // STEP_END + + // STEP_START xrange_step_2 + res15, err := rdb.XRangeN(ctx, "race:france", + "(1692632094485-0", "+", 2, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res15) + // >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // STEP_END + + // STEP_START xrange_empty + res16, err := rdb.XRangeN(ctx, "race:france", + "(1692632147973-0", "+", 2, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res16) + // >>> [] + // STEP_END + + // STEP_START xrevrange + res17, err := rdb.XRevRangeN(ctx, "race:france", "+", "-", 1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res17) + // >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // STEP_END + + // STEP_START xread + res18, err := rdb.XRead(ctx, &redis.XReadArgs{ + Streams: []string{"race:france", "0"}, + Count: 2, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res18) + // >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]}] + // STEP_END + + // Output: + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]} {1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]}] + // [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}] + // [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7]} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [] + // [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9]}] + // [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2]} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8]}]}] +} + +func ExampleClient_xgroupcreate() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:france") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:france", + Values: map[string]interface{}{ + "rider": "Castilla", + "speed": 30.2, + "position": 1, + "location_id": 1, + }, + ID: "1692632086370-0", + }).Result() + + if err != nil { + panic(err) + } + + // STEP_START xgroup_create + res19, err := rdb.XGroupCreate(ctx, "race:france", "france_riders", "$").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res19) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_xgroupcreatemkstream() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + // STEP_START xgroup_create_mkstream + res20, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res20) // >>> OK + // STEP_END + + // Output: + // OK +} + +func ExampleClient_xgroupread() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + _, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START xgroup_read + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Castilla"}, + }).Result() + // >>> 1692632639151-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Royce"}, + }).Result() + // >>> 1692632647899-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Sam-Bodden"}, + }).Result() + // >>> 1692632662819-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Prickett"}, + }).Result() + // >>> 1692632670501-0 + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Norem"}, + }).Result() + // >>> 1692632678249-0 + + if err != nil { + panic(err) + } + + // fmt.Println(res25) + + res21, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Alice", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res21) + // >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // STEP_END + + // REMOVE_START + UNUSED(res21) + // REMOVE_END + + xlen, err := rdb.XLen(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(xlen) + + // Output: + // 5 +} + +func ExampleClient_raceitaly() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + rdb.XGroupDestroy(ctx, "race:italy", "italy_riders") + // REMOVE_END + + _, err := rdb.XGroupCreateMkStream(ctx, + "race:italy", "italy_riders", "$", + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Castilla"}, + ID: "1692632639151-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Royce"}, + ID: "1692632647899-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Sam-Bodden"}, + ID: "1692632662819-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Prickett"}, + ID: "1692632670501-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + Values: map[string]interface{}{"rider": "Norem"}, + ID: "1692632678249-0", + }).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Alice", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + // STEP_START xgroup_read_id + res22, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", "0"}, + Group: "italy_riders", + Consumer: "Alice", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res22) + // >>> [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // STEP_END + + // STEP_START xack + res23, err := rdb.XAck(ctx, + "race:italy", "italy_riders", "1692632639151-0", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res23) // >>> 1 + + res24, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", "0"}, + Group: "italy_riders", + Consumer: "Alice", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res24) + // >>> [{race:italy []}] + // STEP_END + + // STEP_START xgroup_read_bob + res25, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ + Streams: []string{"race:italy", ">"}, + Group: "italy_riders", + Consumer: "Bob", + Count: 2, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res25) + // >>> [{race:italy [{1692632647899-0 map[rider:Royce]} {1692632662819-0 map[rider:Sam-Bodden]}]}] + + // STEP_END + + // STEP_START xpending + res26, err := rdb.XPending(ctx, "race:italy", "italy_riders").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res26) + // >>> &{2 1692632647899-0 1692632662819-0 map[Bob:2]} + // STEP_END + + // STEP_START xpending_plus_minus + res27, err := rdb.XPendingExt(ctx, &redis.XPendingExtArgs{ + Stream: "race:italy", + Group: "italy_riders", + Start: "-", + End: "+", + Count: 10, + }).Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res27) + // >>> [{1692632647899-0 Bob 0s 1} {1692632662819-0 Bob 0s 1}] + // STEP_END + + // STEP_START xrange_pending + res28, err := rdb.XRange(ctx, "race:italy", + "1692632647899-0", "1692632647899-0", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce]}] + // STEP_END + + // STEP_START xclaim + res29, err := rdb.XClaim(ctx, &redis.XClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Alice", + MinIdle: 0, + Messages: []string{"1692632647899-0"}, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res29) + // STEP_END + + // STEP_START xautoclaim + res30, res30a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Alice", + Start: "0-0", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res30) // >>> [{1692632647899-0 map[rider:Royce]}] + fmt.Println(res30a) // >>> 1692632662819-0 + // STEP_END + + // STEP_START xautoclaim_cursor + res31, res31a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ + Stream: "race:italy", + Group: "italy_riders", + Consumer: "Lora", + Start: "(1692632662819-0", + Count: 1, + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res31) // >>> [] + fmt.Println(res31a) // >>> 0-0 + // STEP_END + + // STEP_START xinfo + res32, err := rdb.XInfoStream(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res32) + // >>> &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla]} {1692632678249-0 map[rider:Norem]} 1692632639151-0} + // STEP_END + + // STEP_START xinfo_groups + res33, err := rdb.XInfoGroups(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res33) + // >>> [{italy_riders 3 2 1692632662819-0 3 2}] + // STEP_END + + // STEP_START xinfo_consumers + res34, err := rdb.XInfoConsumers(ctx, "race:italy", "italy_riders").Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res34) + // >>> [{Alice 1 1ms 1ms} {Bob 1 2ms 2ms} {Lora 0 1ms -1ms}] + // STEP_END + + // STEP_START maxlen + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Jones"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Wood"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Henshaw"}, + }, + ).Result() + + if err != nil { + panic(err) + } + + res35, err := rdb.XLen(ctx, "race:italy").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res35) // >>> 2 + + res36, err := rdb.XRange(ctx, "race:italy", "-", "+").Result() + + if err != nil { + panic(err) + } + + // fmt.Println(res36) + // >>> [{1726649529170-1 map[rider:Wood]} {1726649529171-0 map[rider:Henshaw]}] + // STEP_END + + // STEP_START xtrim + res37, err := rdb.XTrimMaxLen(ctx, "race:italy", 10).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res37) // >>> 0 + // STEP_END + + // STEP_START xtrim2 + res38, err := rdb.XTrimMaxLenApprox(ctx, "race:italy", 10, 20).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res38) // >>> 0 + // STEP_END + + // REMOVE_START + UNUSED(res27, res34, res36) + // REMOVE_END + + // Output: + // [{race:italy [{1692632639151-0 map[rider:Castilla]}]}] + // 1 + // [{race:italy []}] + // [{race:italy [{1692632647899-0 map[rider:Royce]} {1692632662819-0 map[rider:Sam-Bodden]}]}] + // &{2 1692632647899-0 1692632662819-0 map[Bob:2]} + // [{1692632647899-0 map[rider:Royce]}] + // [{1692632647899-0 map[rider:Royce]}] + // [{1692632647899-0 map[rider:Royce]}] + // 1692632662819-0 + // [] + // 0-0 + // &{5 1 2 1 1692632678249-0 0-0 5 {1692632639151-0 map[rider:Castilla]} {1692632678249-0 map[rider:Norem]} 1692632639151-0} + // [{italy_riders 3 2 1692632662819-0 3 2}] + // 2 + // 0 + // 0 +} + +func ExampleClient_xdel() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "race:italy") + // REMOVE_END + + _, err := rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Wood"}, + ID: "1692633198206-0", + }, + ).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.XAdd(ctx, &redis.XAddArgs{ + Stream: "race:italy", + MaxLen: 2, + Values: map[string]interface{}{"rider": "Henshaw"}, + ID: "1692633208557-0", + }, + ).Result() + + if err != nil { + panic(err) + } + + // STEP_START xdel + res39, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res39) + // >>> [{1692633198206-0 map[rider:Wood]} {1692633208557-0 map[rider:Henshaw]}] + + res40, err := rdb.XDel(ctx, "race:italy", "1692633208557-0").Result() + + if err != nil { + panic(err) + } + + fmt.Println(res40) // 1 + + res41, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res41) + // >>> [{1692633198206-0 map[rider:Wood]}] + // STEP_END + + // Output: + // [{1692633198206-0 map[rider:Wood]} {1692633208557-0 map[rider:Henshaw]}] + // 1 + // [{1692633198206-0 map[rider:Wood]}] +} From 80c9f5bb777dd4e6393d014cffe9d28428ecf756 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:25:46 +0000 Subject: [PATCH 21/68] DOC-4345 added JSON samples for home page (#3183) --- doctests/home_json_example_test.go | 199 +++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 doctests/home_json_example_test.go diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go new file mode 100644 index 000000000..b9e46a638 --- /dev/null +++ b/doctests/home_json_example_test.go @@ -0,0 +1,199 @@ +// EXAMPLE: go_home_json +// HIDE_START +package example_commands_test + +// HIDE_END +// STEP_START import +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +// STEP_END + +func ExampleClient_search_json() { + // STEP_START connect + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // STEP_END + // REMOVE_START + rdb.Del(ctx, "user:1", "user:2", "user:3") + rdb.FTDropIndex(ctx, "idx:users") + // REMOVE_END + + // STEP_START create_data + user1 := map[string]interface{}{ + "name": "Paul John", + "email": "paul.john@example.com", + "age": 42, + "city": "London", + } + + user2 := map[string]interface{}{ + "name": "Eden Zamir", + "email": "eden.zamir@example.com", + "age": 29, + "city": "Tel Aviv", + } + + user3 := map[string]interface{}{ + "name": "Paul Zamir", + "email": "paul.zamir@example.com", + "age": 35, + "city": "Tel Aviv", + } + // STEP_END + + // STEP_START make_index + _, err := rdb.FTCreate( + ctx, + "idx:users", + // Options: + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"user:"}, + }, + // Index schema fields: + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.city", + As: "city", + FieldType: redis.SearchFieldTypeTag, + }, + &redis.FieldSchema{ + FieldName: "$.age", + As: "age", + FieldType: redis.SearchFieldTypeNumeric, + }, + ).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START add_data + _, err = rdb.JSONSet(ctx, "user:1", "$", user1).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.JSONSet(ctx, "user:2", "$", user2).Result() + + if err != nil { + panic(err) + } + + _, err = rdb.JSONSet(ctx, "user:3", "$", user3).Result() + + if err != nil { + panic(err) + } + // STEP_END + + // STEP_START query1 + findPaulResult, err := rdb.FTSearch( + ctx, + "idx:users", + "Paul @age:[30 40]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(findPaulResult) + // >>> {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv"... + // STEP_END + + // STEP_START query2 + citiesResult, err := rdb.FTSearchWithArgs( + ctx, + "idx:users", + "Paul", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "$.city", + As: "city", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + sort.Slice(citiesResult.Docs, func(i, j int) bool { + return citiesResult.Docs[i].Fields["city"] < citiesResult.Docs[j].Fields["city"] + }) + + for _, result := range citiesResult.Docs { + fmt.Println(result.Fields["city"]) + } + // >>> London + // >>> Tel Aviv + // STEP_END + + // STEP_START query3 + aggOptions := redis.FTAggregateOptions{ + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@city"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchCount, + As: "count", + }, + }, + }, + }, + } + + aggResult, err := rdb.FTAggregateWithArgs( + ctx, + "idx:users", + "*", + &aggOptions, + ).Result() + + if err != nil { + panic(err) + } + + sort.Slice(aggResult.Rows, func(i, j int) bool { + return aggResult.Rows[i].Fields["city"].(string) < + aggResult.Rows[j].Fields["city"].(string) + }) + + for _, row := range aggResult.Rows { + fmt.Printf("%v - %v\n", + row.Fields["city"], row.Fields["count"], + ) + } + // >>> City: London - 1 + // >>> City: Tel Aviv - 2 + // STEP_END + + // Output: + // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} + // London + // Tel Aviv + // London - 1 + // Tel Aviv - 2 +} From d1b4eaed41a0cf43ed0e7129ad7b783971c9629a Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:27:00 +0200 Subject: [PATCH 22/68] Support TimeSeries commands with RESP 2 protocol (#3184) * Support Timeseries resp 2 * Change to resp 2 * Support Resp2 for TimeSeries commands --- command.go | 54 +- timeseries_commands_test.go | 2289 +++++++++++++++++++---------------- 2 files changed, 1270 insertions(+), 1073 deletions(-) diff --git a/command.go b/command.go index 4ced2979d..7ea7862d5 100644 --- a/command.go +++ b/command.go @@ -1403,27 +1403,63 @@ func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} { } func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadMapLen() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val = make(map[string][]interface{}, n) - for i := 0; i < n; i++ { - k, err := rd.ReadString() + + cmd.val = make(map[string][]interface{}) + + if readType == proto.RespMap { + n, err := rd.ReadMapLen() if err != nil { return err } - nn, err := rd.ReadArrayLen() + for i := 0; i < n; i++ { + k, err := rd.ReadString() + if err != nil { + return err + } + nn, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val[k] = make([]interface{}, nn) + for j := 0; j < nn; j++ { + value, err := rd.ReadReply() + if err != nil { + return err + } + cmd.val[k][j] = value + } + } + } else if readType == proto.RespArray { + // RESP2 response + n, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val[k] = make([]interface{}, nn) - for j := 0; j < nn; j++ { - value, err := rd.ReadReply() + + for i := 0; i < n; i++ { + // Each entry in this array is itself an array with key details + itemLen, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val[k][j] = value + + key, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[key] = make([]interface{}, 0, itemLen-1) + for j := 1; j < itemLen; j++ { + // Read the inner array for timestamp-value pairs + data, err := rd.ReadReply() + if err != nil { + return err + } + cmd.val[key] = append(cmd.val[key], data) + } } } diff --git a/timeseries_commands_test.go b/timeseries_commands_test.go index c62367a76..a2d4ba293 100644 --- a/timeseries_commands_test.go +++ b/timeseries_commands_test.go @@ -2,6 +2,7 @@ package redis_test import ( "context" + "fmt" "strings" . "github.com/bsm/ginkgo/v2" @@ -12,1068 +13,1228 @@ import ( var _ = Describe("RedisTimeseries commands", Label("timeseries"), func() { ctx := context.TODO() - var client *redis.Client - - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: rediStackAddr}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - // Test TSCreateWithArgs - opt := &redis.TSOptions{Retention: 5} - result, err = client.TSCreateWithArgs(ctx, "2", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} - result, err = client.TSCreateWithArgs(ctx, "3", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Time": "Series"}, Retention: 20} - result, err = client.TSCreateWithArgs(ctx, "4", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - // Test chunk size - opt = &redis.TSOptions{ChunkSize: 128} - result, err = client.TSCreateWithArgs(ctx, "ts-cs-1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - // Test duplicate policy - duplicate_policies := []string{"BLOCK", "LAST", "FIRST", "MIN", "MAX"} - for _, dup := range duplicate_policies { - keyName := "ts-dup-" + dup - opt = &redis.TSOptions{DuplicatePolicy: dup} - result, err = client.TSCreateWithArgs(ctx, keyName, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err = client.TSInfo(ctx, keyName).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(strings.ToUpper(resultInfo["duplicatePolicy"].(string))).To(BeEquivalentTo(dup)) - } - // Test insertion filters - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: "LAST", IgnoreMaxValDiff: 10.0} - result, err = client.TSCreateWithArgs(ctx, "ts-if-1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1000)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1020, 11.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1020)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1021, 22.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1021)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1021).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(4)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1020, Value: 11.5}, - {Timestamp: 1021, Value: 22.0}})) - // Test insertion filters with other duplicate policy - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0} - result, err = client.TSCreateWithArgs(ctx, "ts-if-2", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultAdd1, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1000)) - resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1010)) - resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd1).To(BeEquivalentTo(1013)) - - rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(3)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1013, Value: 10.0}})) - }) - It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { - result, err := client.TSAdd(ctx, "1", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - // Test TSAddWithArgs - opt := &redis.TSOptions{Retention: 10} - result, err = client.TSAddWithArgs(ctx, "2", 2, 3, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(2)) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} - result, err = client.TSAddWithArgs(ctx, "3", 3, 2, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(3)) - opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs", "Time": "Series"}, Retention: 10} - result, err = client.TSAddWithArgs(ctx, "4", 4, 2, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(4)) - resultInfo, err := client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - // Test chunk size - opt = &redis.TSOptions{ChunkSize: 128} - result, err = client.TSAddWithArgs(ctx, "ts-cs-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - // Test duplicate policy - // LAST - opt = &redis.TSOptions{DuplicatePolicy: "LAST"} - result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err := client.TSGet(ctx, "tsal-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(10)) - // FIRST - opt = &redis.TSOptions{DuplicatePolicy: "FIRST"} - result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsaf-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(5)) - // MAX - opt = &redis.TSOptions{DuplicatePolicy: "MAX"} - result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsam-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(10)) - // MIN - opt = &redis.TSOptions{DuplicatePolicy: "MIN"} - result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1)) - resultGet, err = client.TSGet(ctx, "tsami-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(5)) - // Insertion filters - opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1000, 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1000)) - - result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1004, 3.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(1000)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) - }) - - It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(0)) - - opt := &redis.TSAlterOptions{Retention: 10} - resultAlter, err := client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"]).To(BeEquivalentTo(map[interface{}]interface{}{})) - - opt = &redis.TSAlterOptions{Labels: map[string]string{"Time": "Series"}} - resultAlter, err = client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) - Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) - opt = &redis.TSAlterOptions{DuplicatePolicy: "min"} - resultAlter, err = client.TSAlter(ctx, "1", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultInfo, err = client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("min")) - // Test insertion filters - resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1000)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1010)) - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1013)) - - alterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - resultAlter, err = client.TSAlter(ctx, "ts-if-1", alterOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAlter).To(BeEquivalentTo("OK")) - - resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1015, 11.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(1013)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(3)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ - {Timestamp: 1000, Value: 1.0}, - {Timestamp: 1010, Value: 11.0}, - {Timestamp: 1013, Value: 10.0}})) - }) - - It("should TSCreateRule and TSDeleteRule", Label("timeseries", "tscreaterule", "tsdeleterule"), func() { - result, err := client.TSCreate(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - result, err = client.TSCreate(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - result, err = client.TSCreateRule(ctx, "1", "2", redis.Avg, 100).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo("OK")) - for i := 0; i < 50; i++ { - resultAdd, err := client.TSAdd(ctx, "1", 100+i*2, 1).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 + i*2)) - resultAdd, err = client.TSAdd(ctx, "1", 100+i*2+1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 + i*2 + 1)) - - } - resultAdd, err := client.TSAdd(ctx, "1", 100*2, 1.5).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeEquivalentTo(100 * 2)) - resultGet, err := client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet.Value).To(BeEquivalentTo(1.5)) - Expect(resultGet.Timestamp).To(BeEquivalentTo(100)) - - resultDeleteRule, err := client.TSDeleteRule(ctx, "1", "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultDeleteRule).To(BeEquivalentTo("OK")) - resultInfo, err := client.TSInfo(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["rules"]).To(BeEquivalentTo(map[interface{}]interface{}{})) - }) - - It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSIncrBy(ctx, "1", 1).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err := client.TSGet(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Value).To(BeEquivalentTo(100)) - - for i := 0; i < 100; i++ { - _, err := client.TSDecrBy(ctx, "1", 1).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err = client.TSGet(ctx, "1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Value).To(BeEquivalentTo(0)) - - opt := &redis.TSIncrDecrOptions{Timestamp: 5} - _, err = client.TSIncrByWithArgs(ctx, "2", 1.5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(5)) - Expect(result.Value).To(BeEquivalentTo(1.5)) - - opt = &redis.TSIncrDecrOptions{Timestamp: 7} - _, err = client.TSIncrByWithArgs(ctx, "2", 2.25, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(7)) - Expect(result.Value).To(BeEquivalentTo(3.75)) - - opt = &redis.TSIncrDecrOptions{Timestamp: 15} - _, err = client.TSDecrByWithArgs(ctx, "2", 1.5, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.TSGet(ctx, "2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(15)) - Expect(result.Value).To(BeEquivalentTo(2.25)) - - // Test chunk size INCRBY - opt = &redis.TSIncrDecrOptions{ChunkSize: 128} - _, err = client.TSIncrByWithArgs(ctx, "3", 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TSInfo(ctx, "3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - - // Test chunk size DECRBY - opt = &redis.TSIncrDecrOptions{ChunkSize: 128} - _, err = client.TSDecrByWithArgs(ctx, "4", 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err = client.TSInfo(ctx, "4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) - - // Test insertion filters INCRBY - opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - res, err := client.TSIncrByWithArgs(ctx, "ts-if-1", 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) - - res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}})) - - // Test insertion filters DECRBY - opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 1.0, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}})) - - res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(res).To(BeEquivalentTo(1000)) - - rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rangePoints)).To(BeEquivalentTo(1)) - Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}})) - }) - - It("should TSGet", Label("timeseries", "tsget"), func() { - opt := &redis.TSOptions{DuplicatePolicy: "max"} - resultGet, err := client.TSAddWithArgs(ctx, "foo", 2265985, 151, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo(2265985)) - result, err := client.TSGet(ctx, "foo").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(2265985)) - Expect(result.Value).To(BeEquivalentTo(151)) - }) - - It("should TSGet Latest", Label("timeseries", "tsgetlatest", "NonRedisEnterprise"), func() { - resultGet, err := client.TSCreate(ctx, "tsgl-1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - resultGet, err = client.TSCreate(ctx, "tsgl-2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - - resultGet, err = client.TSCreateRule(ctx, "tsgl-1", "tsgl-2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - - Expect(resultGet).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "tsgl-1", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "tsgl-1", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - result, errGet := client.TSGet(ctx, "tsgl-2").Result() - Expect(errGet).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(0)) - Expect(result.Value).To(BeEquivalentTo(4)) - result, errGet = client.TSGetWithArgs(ctx, "tsgl-2", &redis.TSGetOptions{Latest: true}).Result() - Expect(errGet).NotTo(HaveOccurred()) - Expect(result.Timestamp).To(BeEquivalentTo(10)) - Expect(result.Value).To(BeEquivalentTo(8)) - }) - - It("should TSInfo", Label("timeseries", "tsinfo"), func() { - resultGet, err := client.TSAdd(ctx, "foo", 2265985, 151).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo(2265985)) - result, err := client.TSInfo(ctx, "foo").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["firstTimestamp"]).To(BeEquivalentTo(2265985)) - }) - - It("should TSMAdd", Label("timeseries", "tsmadd"), func() { - resultGet, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultGet).To(BeEquivalentTo("OK")) - ktvSlices := make([][]interface{}, 3) - for i := 0; i < 3; i++ { - ktvSlices[i] = make([]interface{}, 3) - ktvSlices[i][0] = "a" - for j := 1; j < 3; j++ { - ktvSlices[i][j] = (i + j) * j - } - } - result, err := client.TSMAdd(ctx, ktvSlices).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]int64{1, 2, 3})) - }) - - It("should TSMGet and TSMGetWithArgs", Label("timeseries", "tsmget", "tsmgetWithArgs", "NonRedisEnterprise"), func() { - opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "a", "*", 15).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", "*", 25).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.TSMGet(ctx, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo(15)) - Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo(25)) - mgetOpt := &redis.TSMGetOptions{WithLabels: true} - result, err = client.TSMGetWithArgs(ctx, []string{"Test=This"}, mgetOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "Taste": "That"})) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreateRule, err := client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - result, err = client.TSMGet(ctx, []string{"is_compaction=true"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), 4.0})) - mgetOpt = &redis.TSMGetOptions{Latest: true} - result, err = client.TSMGetWithArgs(ctx, []string{"is_compaction=true"}, mgetOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), 8.0})) - }) - - It("should TSQueryIndex", Label("timeseries", "tsqueryindex"), func() { - opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - result, err := client.TSQueryIndex(ctx, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - result, err = client.TSQueryIndex(ctx, []string{"Taste=That"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(1)) - }) - - It("should TSDel and TSRange", Label("timeseries", "tsdel", "tsrange"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - resultDelete, err := client.TSDel(ctx, "a", 0, 21).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultDelete).To(BeEquivalentTo(22)) - - resultRange, err := client.TSRange(ctx, "a", 0, 21).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange).To(BeEquivalentTo([]redis.TSTimestampValue{})) - - resultRange, err = client.TSRange(ctx, "a", 22, 22).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 22, Value: 1})) - }) - - It("should TSRange, TSRangeWithArgs", Label("timeseries", "tsrange", "tsrangeWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - - } - result, err := client.TSRange(ctx, "a", 0, 200).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(100)) - for i := 0; i < 100; i++ { - client.TSAdd(ctx, "a", i+200, float64(i%7)) - } - result, err = client.TSRange(ctx, "a", 0, 500).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(200)) - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - opt := &redis.TSRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 10}, {Timestamp: 10, Value: 1}})) - opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "5"} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 5}, {Timestamp: 5, Value: 6}})) - opt = &redis.TSRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} - result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 2.55}, {Timestamp: 10, Value: 3}})) - // Test Range Latest - resultCreate, err := client.TSCreate(ctx, "t1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreate, err = client.TSCreate(ctx, "t2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRule).To(BeEquivalentTo("OK")) - _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - resultRange, err := client.TSRange(ctx, "t1", 0, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 1, Value: 1})) - - opt = &redis.TSRangeOptions{Latest: true} - resultRange, err = client.TSRangeWithArgs(ctx, "t2", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - // Test Bucket Timestamp - resultCreate, err = client.TSCreate(ctx, "t3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} - resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 20, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - // Test Empty - _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} - resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) - Expect(len(resultRange)).To(BeEquivalentTo(7)) - }) - - It("should TSRevRange, TSRevRangeWithArgs", Label("timeseries", "tsrevrange", "tsrevrangeWithArgs", "NonRedisEnterprise"), func() { - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - - } - result, err := client.TSRange(ctx, "a", 0, 200).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(100)) - for i := 0; i < 100; i++ { - client.TSAdd(ctx, "a", i+200, float64(i%7)) - } - result, err = client.TSRange(ctx, "a", 0, 500).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(200)) - - opt := &redis.TSRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(20)) - - opt = &redis.TSRevRangeOptions{Count: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(10)) - - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - opt = &redis.TSRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 1}, {Timestamp: 0, Value: 10}})) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "1"} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1, Value: 10}, {Timestamp: 0, Value: 1}})) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} - result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 3}, {Timestamp: 0, Value: 2.55}})) - // Test Range Latest - resultCreate, err := client.TSCreate(ctx, "t1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultCreate, err = client.TSCreate(ctx, "t2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRule).To(BeEquivalentTo("OK")) - _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - resultRange, err := client.TSRange(ctx, "t2", 0, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - opt = &redis.TSRevRangeOptions{Latest: true} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 10, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 8})) - resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 9, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) - // Test Bucket Timestamp - resultCreate, err = client.TSCreate(ctx, "t3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 80, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - // Test Empty - _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() - Expect(errAdd).NotTo(HaveOccurred()) - _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() - Expect(errAdd).NotTo(HaveOccurred()) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(3)) - - opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} - resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) - Expect(len(resultRange)).To(BeEquivalentTo(7)) - }) - - It("should TSMRange and TSMRangeWithArgs", Label("timeseries", "tsmrange", "tsmrangeWithArgs"), func() { - createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() - Expect(err).NotTo(HaveOccurred()) - } - - result, err := client.TSMRange(ctx, 0, 200, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) - // Test Count - mrangeOpt := &redis.TSMRangeOptions{Count: 10} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) - // Test Aggregation and BucketDuration - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSMRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) - // Test WithLabels - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - mrangeOpt = &redis.TSMRangeOptions{WithLabels: true} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) - // Test SelectedLabels - mrangeOpt = &redis.TSMRangeOptions{SelectedLabels: []interface{}{"team"}} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) - // Test FilterBy - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - mrangeOpt = &redis.TSMRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), 1.0}, []interface{}{int64(16), 2.0}})) - // Test GroupBy - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "sum"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(3), 6.0}})) - - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "max"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - - mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "team", Reducer: "min"} - result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) - // Test Align - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} - result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 10.0}, []interface{}{int64(10), 1.0}})) - - mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 5} - result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 5.0}, []interface{}{int64(5), 6.0}})) - }) - - It("should TSMRangeWithArgs Latest", Label("timeseries", "tsmrangeWithArgs", "tsmrangelatest", "NonRedisEnterprise"), func() { - resultCreate, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - - _, err = client.TSAdd(ctx, "a", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - mrangeOpt := &redis.TSMRangeOptions{Latest: true} - result, err := client.TSMRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) - Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) - }) - It("should TSMRevRange and TSMRevRangeWithArgs", Label("timeseries", "tsmrevrange", "tsmrevrangeWithArgs"), func() { - createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} - resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() - Expect(err).NotTo(HaveOccurred()) - } - result, err := client.TSMRevRange(ctx, 0, 200, []string{"Test=This"}).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) - // Test Count - mrangeOpt := &redis.TSMRevRangeOptions{Count: 10} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) - // Test Aggregation and BucketDuration - for i := 0; i < 100; i++ { - _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() - Expect(err).NotTo(HaveOccurred()) - } - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - // Test WithLabels - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) - mrangeOpt = &redis.TSMRevRangeOptions{WithLabels: true} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) - // Test SelectedLabels - mrangeOpt = &redis.TSMRevRangeOptions{SelectedLabels: []interface{}{"team"}} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) - Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) - // Test FilterBy - fts := make([]int, 0) - for i := 10; i < 20; i++ { - fts = append(fts, i) - } - mrangeOpt = &redis.TSMRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(16), 2.0}, []interface{}{int64(15), 1.0}})) - // Test GroupBy - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "sum"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 6.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(0), 0.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "max"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "team", Reducer: "min"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(2)) - Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) - // Test Align - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 1.0}, []interface{}{int64(0), 10.0}})) - - mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 1} - result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), 10.0}, []interface{}{int64(0), 1.0}})) - }) - - It("should TSMRevRangeWithArgs Latest", Label("timeseries", "tsmrevrangeWithArgs", "tsmrevrangelatest", "NonRedisEnterprise"), func() { - resultCreate, err := client.TSCreate(ctx, "a").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreate, err = client.TSCreate(ctx, "c").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} - resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreate).To(BeEquivalentTo("OK")) - - resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultCreateRule).To(BeEquivalentTo("OK")) - - _, err = client.TSAdd(ctx, "a", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "a", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.TSAdd(ctx, "c", 1, 1).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 11, 7).Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.TSAdd(ctx, "c", 13, 1).Result() - Expect(err).NotTo(HaveOccurred()) - mrangeOpt := &redis.TSMRevRangeOptions{Latest: true} - result, err := client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) - Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) - }) + + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, + UnstableResp3: true, + }) + } + + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol // capture loop variable for each context + + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client + + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() + } + }) + + It("should TSCreate and TSCreateWithArgs", Label("timeseries", "tscreate", "tscreateWithArgs", "NonRedisEnterprise"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + // Test TSCreateWithArgs + opt := &redis.TSOptions{Retention: 5} + result, err = client.TSCreateWithArgs(ctx, "2", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} + result, err = client.TSCreateWithArgs(ctx, "3", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Time": "Series"}, Retention: 20} + result, err = client.TSCreateWithArgs(ctx, "4", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"Time", "Series"})) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + } + // Test chunk size + opt = &redis.TSOptions{ChunkSize: 128} + result, err = client.TSCreateWithArgs(ctx, "ts-cs-1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + // Test duplicate policy + duplicate_policies := []string{"BLOCK", "LAST", "FIRST", "MIN", "MAX"} + for _, dup := range duplicate_policies { + keyName := "ts-dup-" + dup + opt = &redis.TSOptions{DuplicatePolicy: dup} + result, err = client.TSCreateWithArgs(ctx, keyName, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err = client.TSInfo(ctx, keyName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(strings.ToUpper(resultInfo["duplicatePolicy"].(string))).To(BeEquivalentTo(dup)) + } + // Test insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, DuplicatePolicy: "LAST", IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1020, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1020)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1021, 22.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1021)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1021).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(4)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1020, Value: 11.5}, + {Timestamp: 1021, Value: 22.0}})) + // Test insertion filters with other duplicate policy + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0} + result, err = client.TSCreateWithArgs(ctx, "ts-if-2", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultAdd1, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1000)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1010)) + resultAdd1, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd1).To(BeEquivalentTo(1013)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) + }) + It("should TSAdd and TSAddWithArgs", Label("timeseries", "tsadd", "tsaddWithArgs", "NonRedisEnterprise"), func() { + result, err := client.TSAdd(ctx, "1", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + // Test TSAddWithArgs + opt := &redis.TSOptions{Retention: 10} + result, err = client.TSAddWithArgs(ctx, "2", 2, 3, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(2)) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs"}} + result, err = client.TSAddWithArgs(ctx, "3", 3, 2, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(3)) + opt = &redis.TSOptions{Labels: map[string]string{"Redis": "Labs", "Time": "Series"}, Retention: 10} + result, err = client.TSAddWithArgs(ctx, "4", 4, 2, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(4)) + resultInfo, err := client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})).To(ContainElement([]interface{}{"Time", "Series"})) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + } + // Test chunk size + opt = &redis.TSOptions{ChunkSize: 128} + result, err = client.TSAddWithArgs(ctx, "ts-cs-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultInfo, err = client.TSInfo(ctx, "ts-cs-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + // Test duplicate policy + // LAST + opt = &redis.TSOptions{DuplicatePolicy: "LAST"} + result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsal-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err := client.TSGet(ctx, "tsal-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(10)) + // FIRST + opt = &redis.TSOptions{DuplicatePolicy: "FIRST"} + result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsaf-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsaf-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(5)) + // MAX + opt = &redis.TSOptions{DuplicatePolicy: "MAX"} + result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsam-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsam-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(10)) + // MIN + opt = &redis.TSOptions{DuplicatePolicy: "MIN"} + result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + result, err = client.TSAddWithArgs(ctx, "tsami-1", 1, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1)) + resultGet, err = client.TSGet(ctx, "tsami-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(5)) + // Insertion filters + opt = &redis.TSOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1000, 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + result, err = client.TSAddWithArgs(ctx, "ts-if-1", 1004, 3.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) + }) + + It("should TSAlter", Label("timeseries", "tsalter", "NonRedisEnterprise"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(0)) + + opt := &redis.TSAlterOptions{Retention: 10} + resultAlter, err := client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(resultInfo["labels"]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + + opt = &redis.TSAlterOptions{Labels: map[string]string{"Time": "Series"}} + resultAlter, err = client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["labels"].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"Time", "Series"})) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } else { + Expect(resultInfo["labels"].(map[interface{}]interface{})["Time"]).To(BeEquivalentTo("Series")) + Expect(resultInfo["retentionTime"]).To(BeEquivalentTo(10)) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo(redis.Nil)) + } + opt = &redis.TSAlterOptions{DuplicatePolicy: "min"} + resultAlter, err = client.TSAlter(ctx, "1", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultInfo, err = client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["duplicatePolicy"]).To(BeEquivalentTo("min")) + // Test insertion filters + resultAdd, err := client.TSAdd(ctx, "ts-if-1", 1000, 1.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1000)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1010, 11.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1010)) + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1013, 10.0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + alterOpt := &redis.TSAlterOptions{IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + resultAlter, err = client.TSAlter(ctx, "ts-if-1", alterOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAlter).To(BeEquivalentTo("OK")) + + resultAdd, err = client.TSAdd(ctx, "ts-if-1", 1015, 11.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(1013)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1013).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(3)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{ + {Timestamp: 1000, Value: 1.0}, + {Timestamp: 1010, Value: 11.0}, + {Timestamp: 1013, Value: 10.0}})) + }) + + It("should TSCreateRule and TSDeleteRule", Label("timeseries", "tscreaterule", "tsdeleterule"), func() { + result, err := client.TSCreate(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + result, err = client.TSCreate(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + result, err = client.TSCreateRule(ctx, "1", "2", redis.Avg, 100).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo("OK")) + for i := 0; i < 50; i++ { + resultAdd, err := client.TSAdd(ctx, "1", 100+i*2, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 + i*2)) + resultAdd, err = client.TSAdd(ctx, "1", 100+i*2+1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 + i*2 + 1)) + + } + resultAdd, err := client.TSAdd(ctx, "1", 100*2, 1.5).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeEquivalentTo(100 * 2)) + resultGet, err := client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet.Value).To(BeEquivalentTo(1.5)) + Expect(resultGet.Timestamp).To(BeEquivalentTo(100)) + + resultDeleteRule, err := client.TSDeleteRule(ctx, "1", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultDeleteRule).To(BeEquivalentTo("OK")) + resultInfo, err := client.TSInfo(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(resultInfo["rules"]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(resultInfo["rules"]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + }) + + It("should TSIncrBy, TSIncrByWithArgs, TSDecrBy and TSDecrByWithArgs", Label("timeseries", "tsincrby", "tsdecrby", "tsincrbyWithArgs", "tsdecrbyWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSIncrBy(ctx, "1", 1).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err := client.TSGet(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Value).To(BeEquivalentTo(100)) + + for i := 0; i < 100; i++ { + _, err := client.TSDecrBy(ctx, "1", 1).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err = client.TSGet(ctx, "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Value).To(BeEquivalentTo(0)) + + opt := &redis.TSIncrDecrOptions{Timestamp: 5} + _, err = client.TSIncrByWithArgs(ctx, "2", 1.5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(5)) + Expect(result.Value).To(BeEquivalentTo(1.5)) + + opt = &redis.TSIncrDecrOptions{Timestamp: 7} + _, err = client.TSIncrByWithArgs(ctx, "2", 2.25, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(7)) + Expect(result.Value).To(BeEquivalentTo(3.75)) + + opt = &redis.TSIncrDecrOptions{Timestamp: 15} + _, err = client.TSDecrByWithArgs(ctx, "2", 1.5, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.TSGet(ctx, "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(15)) + Expect(result.Value).To(BeEquivalentTo(2.25)) + + // Test chunk size INCRBY + opt = &redis.TSIncrDecrOptions{ChunkSize: 128} + _, err = client.TSIncrByWithArgs(ctx, "3", 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TSInfo(ctx, "3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + + // Test chunk size DECRBY + opt = &redis.TSIncrDecrOptions{ChunkSize: 128} + _, err = client.TSDecrByWithArgs(ctx, "4", 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err = client.TSInfo(ctx, "4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo["chunkSize"]).To(BeEquivalentTo(128)) + + // Test insertion filters INCRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err := client.TSIncrByWithArgs(ctx, "ts-if-1", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err := client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 1.0}})) + + res, err = client.TSIncrByWithArgs(ctx, "ts-if-1", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-1", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: 11.1}})) + + // Test insertion filters DECRBY + opt = &redis.TSIncrDecrOptions{Timestamp: 1000, IgnoreMaxTimeDiff: 5, IgnoreMaxValDiff: 10.0, DuplicatePolicy: "LAST"} + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 1.0, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 3.0, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -1.0}})) + + res, err = client.TSDecrByWithArgs(ctx, "ts-if-2", 10.1, &redis.TSIncrDecrOptions{Timestamp: 1000}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo(1000)) + + rangePoints, err = client.TSRange(ctx, "ts-if-2", 1000, 1004).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rangePoints)).To(BeEquivalentTo(1)) + Expect(rangePoints).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1000, Value: -11.1}})) + }) + + It("should TSGet", Label("timeseries", "tsget"), func() { + opt := &redis.TSOptions{DuplicatePolicy: "max"} + resultGet, err := client.TSAddWithArgs(ctx, "foo", 2265985, 151, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo(2265985)) + result, err := client.TSGet(ctx, "foo").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(2265985)) + Expect(result.Value).To(BeEquivalentTo(151)) + }) + + It("should TSGet Latest", Label("timeseries", "tsgetlatest", "NonRedisEnterprise"), func() { + resultGet, err := client.TSCreate(ctx, "tsgl-1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + resultGet, err = client.TSCreate(ctx, "tsgl-2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + + resultGet, err = client.TSCreateRule(ctx, "tsgl-1", "tsgl-2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + + Expect(resultGet).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "tsgl-1", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "tsgl-1", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + result, errGet := client.TSGet(ctx, "tsgl-2").Result() + Expect(errGet).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(0)) + Expect(result.Value).To(BeEquivalentTo(4)) + result, errGet = client.TSGetWithArgs(ctx, "tsgl-2", &redis.TSGetOptions{Latest: true}).Result() + Expect(errGet).NotTo(HaveOccurred()) + Expect(result.Timestamp).To(BeEquivalentTo(10)) + Expect(result.Value).To(BeEquivalentTo(8)) + }) + + It("should TSInfo", Label("timeseries", "tsinfo"), func() { + resultGet, err := client.TSAdd(ctx, "foo", 2265985, 151).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo(2265985)) + result, err := client.TSInfo(ctx, "foo").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result["firstTimestamp"]).To(BeEquivalentTo(2265985)) + }) + + It("should TSMAdd", Label("timeseries", "tsmadd"), func() { + resultGet, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultGet).To(BeEquivalentTo("OK")) + ktvSlices := make([][]interface{}, 3) + for i := 0; i < 3; i++ { + ktvSlices[i] = make([]interface{}, 3) + ktvSlices[i][0] = "a" + for j := 1; j < 3; j++ { + ktvSlices[i][j] = (i + j) * j + } + } + result, err := client.TSMAdd(ctx, ktvSlices).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]int64{1, 2, 3})) + }) + + It("should TSMGet and TSMGetWithArgs", Label("timeseries", "tsmget", "tsmgetWithArgs", "NonRedisEnterprise"), func() { + opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "a", "*", 15).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", "*", 25).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.TSMGet(ctx, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo("15")) + Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo("25")) + } else { + Expect(result["a"][1].([]interface{})[1]).To(BeEquivalentTo(15)) + Expect(result["b"][1].([]interface{})[1]).To(BeEquivalentTo(25)) + } + mgetOpt := &redis.TSMGetOptions{WithLabels: true} + result, err = client.TSMGetWithArgs(ctx, []string{"Test=This"}, mgetOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][0]).To(ConsistOf([]interface{}{"Test", "This"}, []interface{}{"Taste", "That"})) + } else { + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "Taste": "That"})) + } + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreateRule, err := client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + result, err = client.TSMGet(ctx, []string{"is_compaction=true"}).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), "4"})) + } else { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(0), 4.0})) + } + mgetOpt = &redis.TSMGetOptions{Latest: true} + result, err = client.TSMGetWithArgs(ctx, []string{"is_compaction=true"}, mgetOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), "8"})) + } else { + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{int64(10), 8.0})) + } + }) + + It("should TSQueryIndex", Label("timeseries", "tsqueryindex"), func() { + opt := &redis.TSOptions{Labels: map[string]string{"Test": "This"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + result, err := client.TSQueryIndex(ctx, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + result, err = client.TSQueryIndex(ctx, []string{"Taste=That"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(1)) + }) + + It("should TSDel and TSRange", Label("timeseries", "tsdel", "tsrange"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + resultDelete, err := client.TSDel(ctx, "a", 0, 21).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultDelete).To(BeEquivalentTo(22)) + + resultRange, err := client.TSRange(ctx, "a", 0, 21).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange).To(BeEquivalentTo([]redis.TSTimestampValue{})) + + resultRange, err = client.TSRange(ctx, "a", 22, 22).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 22, Value: 1})) + }) + + It("should TSRange, TSRangeWithArgs", Label("timeseries", "tsrange", "tsrangeWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + + } + result, err := client.TSRange(ctx, "a", 0, 200).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(100)) + for i := 0; i < 100; i++ { + client.TSAdd(ctx, "a", i+200, float64(i%7)) + } + result, err = client.TSRange(ctx, "a", 0, 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(200)) + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + opt := &redis.TSRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 10}, {Timestamp: 10, Value: 1}})) + opt = &redis.TSRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "5"} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 5}, {Timestamp: 5, Value: 6}})) + opt = &redis.TSRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} + result, err = client.TSRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 0, Value: 2.55}, {Timestamp: 10, Value: 3}})) + // Test Range Latest + resultCreate, err := client.TSCreate(ctx, "t1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreate, err = client.TSCreate(ctx, "t2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRule).To(BeEquivalentTo("OK")) + _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + resultRange, err := client.TSRange(ctx, "t1", 0, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 1, Value: 1})) + + opt = &redis.TSRangeOptions{Latest: true} + resultRange, err = client.TSRangeWithArgs(ctx, "t2", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + // Test Bucket Timestamp + resultCreate, err = client.TSCreate(ctx, "t3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} + resultRange, err = client.TSRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 20, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + // Test Empty + _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} + resultRange, err = client.TSRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 4})) + Expect(len(resultRange)).To(BeEquivalentTo(7)) + }) + + It("should TSRevRange, TSRevRangeWithArgs", Label("timeseries", "tsrevrange", "tsrevrangeWithArgs", "NonRedisEnterprise"), func() { + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + + } + result, err := client.TSRange(ctx, "a", 0, 200).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(100)) + for i := 0; i < 100; i++ { + client.TSAdd(ctx, "a", i+200, float64(i%7)) + } + result, err = client.TSRange(ctx, "a", 0, 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(200)) + + opt := &redis.TSRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(20)) + + opt = &redis.TSRevRangeOptions{Count: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(10)) + + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + opt = &redis.TSRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 500, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "+"} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 1}, {Timestamp: 0, Value: 10}})) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "1"} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 1, Value: 10}, {Timestamp: 0, Value: 1}})) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Twa, BucketDuration: 10} + result, err = client.TSRevRangeWithArgs(ctx, "a", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo([]redis.TSTimestampValue{{Timestamp: 10, Value: 3}, {Timestamp: 0, Value: 2.55}})) + // Test Range Latest + resultCreate, err := client.TSCreate(ctx, "t1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultCreate, err = client.TSCreate(ctx, "t2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + resultRule, err := client.TSCreateRule(ctx, "t1", "t2", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRule).To(BeEquivalentTo("OK")) + _, errAdd := client.TSAdd(ctx, "t1", 1, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 2, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 11, 7).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t1", 13, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + resultRange, err := client.TSRange(ctx, "t2", 0, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + opt = &redis.TSRevRangeOptions{Latest: true} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 10, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 10, Value: 8})) + resultRange, err = client.TSRevRangeWithArgs(ctx, "t2", 0, 9, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 0, Value: 4})) + // Test Bucket Timestamp + resultCreate, err = client.TSCreate(ctx, "t3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + _, errAdd = client.TSAdd(ctx, "t3", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t3", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, BucketTimestamp: "+"} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t3", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 80, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + // Test Empty + _, errAdd = client.TSAdd(ctx, "t4", 15, 1).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 17, 4).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 51, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 73, 5).Result() + Expect(errAdd).NotTo(HaveOccurred()) + _, errAdd = client.TSAdd(ctx, "t4", 75, 3).Result() + Expect(errAdd).NotTo(HaveOccurred()) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(3)) + + opt = &redis.TSRevRangeOptions{Aggregator: redis.Max, Align: 0, BucketDuration: 10, Empty: true} + resultRange, err = client.TSRevRangeWithArgs(ctx, "t4", 0, 100, opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultRange[0]).To(BeEquivalentTo(redis.TSTimestampValue{Timestamp: 70, Value: 5})) + Expect(len(resultRange)).To(BeEquivalentTo(7)) + }) + + It("should TSMRange and TSMRangeWithArgs", Label("timeseries", "tsmrange", "tsmrangeWithArgs"), func() { + createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + result, err := client.TSMRange(ctx, 0, 200, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(100)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) + } + // Test Count + mrangeOpt := &redis.TSMRangeOptions{Count: 10} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(10)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) + } + // Test Aggregation and BucketDuration + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSMRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(20)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) + } + // Test WithLabels + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + mrangeOpt = &redis.TSMRangeOptions{WithLabels: true} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(ConsistOf([]interface{}{[]interface{}{"Test", "This"}, []interface{}{"team", "ny"}})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) + } + // Test SelectedLabels + mrangeOpt = &redis.TSMRangeOptions{SelectedLabels: []interface{}{"team"}} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "ny"})) + Expect(result["b"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "sf"})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) + } + // Test FilterBy + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + mrangeOpt = &redis.TSMRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSMRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), "1"}, []interface{}{int64(16), "2"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(15), 1.0}, []interface{}{int64(16), 2.0}})) + } + // Test GroupBy + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "sum"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "2"}, []interface{}{int64(2), "4"}, []interface{}{int64(3), "6"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(3), 6.0}})) + } + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "Test", Reducer: "max"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + } + + mrangeOpt = &redis.TSMRangeOptions{GroupByLabel: "team", Reducer: "min"} + result, err = client.TSMRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(result["team=ny"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + Expect(result["team=sf"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "0"}, []interface{}{int64(1), "1"}, []interface{}{int64(2), "2"}, []interface{}{int64(3), "3"}})) + } else { + Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 0.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(3), 3.0}})) + } + // Test Align + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} + result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "10"}, []interface{}{int64(10), "1"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 10.0}, []interface{}{int64(10), 1.0}})) + } + + mrangeOpt = &redis.TSMRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 5} + result, err = client.TSMRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), "5"}, []interface{}{int64(5), "6"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 5.0}, []interface{}{int64(5), 6.0}})) + } + }) + + It("should TSMRangeWithArgs Latest", Label("timeseries", "tsmrangeWithArgs", "tsmrangelatest", "NonRedisEnterprise"), func() { + resultCreate, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + + _, err = client.TSAdd(ctx, "a", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + mrangeOpt := &redis.TSMRangeOptions{Latest: true} + result, err := client.TSMRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][1]).To(ConsistOf([]interface{}{int64(0), "4"}, []interface{}{int64(10), "8"})) + Expect(result["d"][1]).To(ConsistOf([]interface{}{int64(0), "4"}, []interface{}{int64(10), "8"})) + } else { + Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) + Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(0), 4.0}, []interface{}{int64(10), 8.0}})) + } + }) + It("should TSMRevRange and TSMRevRangeWithArgs", Label("timeseries", "tsmrevrange", "tsmrevrangeWithArgs"), func() { + createOpt := &redis.TSOptions{Labels: map[string]string{"Test": "This", "team": "ny"}} + resultCreate, err := client.TSCreateWithArgs(ctx, "a", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + createOpt = &redis.TSOptions{Labels: map[string]string{"Test": "This", "Taste": "That", "team": "sf"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", createOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "b", i, float64(i%11)).Result() + Expect(err).NotTo(HaveOccurred()) + } + result, err := client.TSMRevRange(ctx, 0, 200, []string{"Test=This"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(100)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(100)) + } + // Test Count + mrangeOpt := &redis.TSMRevRangeOptions{Count: 10} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(10)) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(10)) + } + // Test Aggregation and BucketDuration + for i := 0; i < 100; i++ { + _, err := client.TSAdd(ctx, "a", i+200, float64(i%7)).Result() + Expect(err).NotTo(HaveOccurred()) + } + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Avg, BucketDuration: 10} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 500, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(len(result["a"][1].([]interface{}))).To(BeEquivalentTo(20)) + Expect(result["a"][0]).To(BeEquivalentTo([]interface{}{})) + } else { + Expect(len(result["a"][2].([]interface{}))).To(BeEquivalentTo(20)) + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{})) + } + mrangeOpt = &redis.TSMRevRangeOptions{WithLabels: true} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0]).To(ConsistOf([]interface{}{[]interface{}{"Test", "This"}, []interface{}{"team", "ny"}})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"Test": "This", "team": "ny"})) + } + // Test SelectedLabels + mrangeOpt = &redis.TSMRevRangeOptions{SelectedLabels: []interface{}{"team"}} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "ny"})) + Expect(result["b"][0].([]interface{})[0]).To(BeEquivalentTo([]interface{}{"team", "sf"})) + } else { + Expect(result["a"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "ny"})) + Expect(result["b"][0]).To(BeEquivalentTo(map[interface{}]interface{}{"team": "sf"})) + } + // Test FilterBy + fts := make([]int, 0) + for i := 10; i < 20; i++ { + fts = append(fts, i) + } + mrangeOpt = &redis.TSMRevRangeOptions{FilterByTS: fts, FilterByValue: []int{1, 2}} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 200, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1].([]interface{})).To(ConsistOf([]interface{}{int64(16), "2"}, []interface{}{int64(15), "1"})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(16), 2.0}, []interface{}{int64(15), 1.0}})) + } + // Test GroupBy + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "sum"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "6"}, []interface{}{int64(2), "4"}, []interface{}{int64(1), "2"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 6.0}, []interface{}{int64(2), 4.0}, []interface{}{int64(1), 2.0}, []interface{}{int64(0), 0.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "Test", Reducer: "max"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["Test=This"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["Test=This"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{GroupByLabel: "team", Reducer: "min"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 3, []string{"Test=This"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(2)) + if client.Options().Protocol == 2 { + Expect(result["team=ny"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + Expect(result["team=sf"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), "3"}, []interface{}{int64(2), "2"}, []interface{}{int64(1), "1"}, []interface{}{int64(0), "0"}})) + } else { + Expect(result["team=ny"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + Expect(result["team=sf"][3]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(3), 3.0}, []interface{}{int64(2), 2.0}, []interface{}{int64(1), 1.0}, []interface{}{int64(0), 0.0}})) + } + // Test Align + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: "-"} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "1"}, []interface{}{int64(0), "10"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 1.0}, []interface{}{int64(0), 10.0}})) + } + mrangeOpt = &redis.TSMRevRangeOptions{Aggregator: redis.Count, BucketDuration: 10, Align: 1} + result, err = client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"team=ny"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["a"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), "10"}, []interface{}{int64(0), "1"}})) + } else { + Expect(result["a"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(1), 10.0}, []interface{}{int64(0), 1.0}})) + } + }) + + It("should TSMRevRangeWithArgs Latest", Label("timeseries", "tsmrevrangeWithArgs", "tsmrevrangelatest", "NonRedisEnterprise"), func() { + resultCreate, err := client.TSCreate(ctx, "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt := &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "b", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreate, err = client.TSCreate(ctx, "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + opt = &redis.TSOptions{Labels: map[string]string{"is_compaction": "true"}} + resultCreate, err = client.TSCreateWithArgs(ctx, "d", opt).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreate).To(BeEquivalentTo("OK")) + + resultCreateRule, err := client.TSCreateRule(ctx, "a", "b", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + resultCreateRule, err = client.TSCreateRule(ctx, "c", "d", redis.Sum, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultCreateRule).To(BeEquivalentTo("OK")) + + _, err = client.TSAdd(ctx, "a", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "a", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.TSAdd(ctx, "c", 1, 1).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 11, 7).Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.TSAdd(ctx, "c", 13, 1).Result() + Expect(err).NotTo(HaveOccurred()) + mrangeOpt := &redis.TSMRevRangeOptions{Latest: true} + result, err := client.TSMRevRangeWithArgs(ctx, 0, 10, []string{"is_compaction=true"}, mrangeOpt).Result() + Expect(err).NotTo(HaveOccurred()) + if client.Options().Protocol == 2 { + Expect(result["b"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "8"}, []interface{}{int64(0), "4"}})) + Expect(result["d"][1]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), "8"}, []interface{}{int64(0), "4"}})) + } else { + Expect(result["b"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) + Expect(result["d"][2]).To(BeEquivalentTo([]interface{}{[]interface{}{int64(10), 8.0}, []interface{}{int64(0), 4.0}})) + } + }) + }) + } }) From 8b1073d2d63a365909e584d57a4997a88f5ed185 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:15:19 +0200 Subject: [PATCH 23/68] Support Probabilistic commands with RESP 2 protocol (#3176) * Support bloom resp 2 * Support Resp2 for BF.Info * simplify BFInfoCmd field assignment using map-based key-to-field references --- probabilistic.go | 72 ++- probabilistic_test.go | 1437 +++++++++++++++++++++-------------------- 2 files changed, 780 insertions(+), 729 deletions(-) diff --git a/probabilistic.go b/probabilistic.go index 5d5cd1a62..02ca263cb 100644 --- a/probabilistic.go +++ b/probabilistic.go @@ -319,37 +319,69 @@ func (cmd *BFInfoCmd) Result() (BFInfo, error) { } func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadMapLen() + result := BFInfo{} + + // Create a mapping from key names to pointers of struct fields + respMapping := map[string]*int64{ + "Capacity": &result.Capacity, + "CAPACITY": &result.Capacity, + "Size": &result.Size, + "SIZE": &result.Size, + "Number of filters": &result.Filters, + "FILTERS": &result.Filters, + "Number of items inserted": &result.ItemsInserted, + "ITEMS": &result.ItemsInserted, + "Expansion rate": &result.ExpansionRate, + "EXPANSION": &result.ExpansionRate, + } + + // Helper function to read and assign a value based on the key + readAndAssignValue := func(key string) error { + fieldPtr, exists := respMapping[key] + if !exists { + return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key) + } + + // Read the integer and assign to the field via pointer dereferencing + val, err := rd.ReadInt() + if err != nil { + return err + } + *fieldPtr = val + return nil + } + + readType, err := rd.PeekReplyType() if err != nil { return err } - var key string - var result BFInfo - for f := 0; f < n; f++ { - key, err = rd.ReadString() + if len(cmd.args) > 2 && readType == proto.RespArray { + n, err := rd.ReadArrayLen() if err != nil { return err } - - switch key { - case "Capacity": - result.Capacity, err = rd.ReadInt() - case "Size": - result.Size, err = rd.ReadInt() - case "Number of filters": - result.Filters, err = rd.ReadInt() - case "Number of items inserted": - result.ItemsInserted, err = rd.ReadInt() - case "Expansion rate": - result.ExpansionRate, err = rd.ReadInt() - default: - return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key) + if key, ok := cmd.args[2].(string); ok && n == 1 { + if err := readAndAssignValue(key); err != nil { + return err + } + } else { + return fmt.Errorf("redis: BLOOM.INFO invalid argument key type") } - + } else { + n, err := rd.ReadMapLen() if err != nil { return err } + for i := 0; i < n; i++ { + key, err := rd.ReadString() + if err != nil { + return err + } + if err := readAndAssignValue(key); err != nil { + return err + } + } } cmd.val = result diff --git a/probabilistic_test.go b/probabilistic_test.go index 0610c515e..a0a050e23 100644 --- a/probabilistic_test.go +++ b/probabilistic_test.go @@ -13,721 +13,740 @@ import ( var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { ctx := context.TODO() - var client *redis.Client - BeforeEach(func() { - client = redis.NewClient(&redis.Options{Addr: ":6379"}) - Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(client.Close()).NotTo(HaveOccurred()) - }) - - Describe("bloom", Label("bloom"), func() { - It("should BFAdd", Label("bloom", "bfadd"), func() { - resultAdd, err := client.BFAdd(ctx, "testbf1", 1).Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd).To(BeTrue()) - - resultInfo, err := client.BFInfo(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(1))) - }) - - It("should BFCard", Label("bloom", "bfcard"), func() { - // This is a probabilistic data structure, and it's not always guaranteed that we will get back - // the exact number of inserted items, during hash collisions - // But with such a low number of items (only 3), - // the probability of a collision is very low, so we can expect to get back the exact number of items - _, err := client.BFAdd(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.BFAdd(ctx, "testbf1", "item2").Result() - Expect(err).NotTo(HaveOccurred()) - _, err = client.BFAdd(ctx, "testbf1", 3).Result() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFCard(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeEquivalentTo(int64(3))) - }) - - It("should BFExists", Label("bloom", "bfexists"), func() { - exists, err := client.BFExists(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - - _, err = client.BFAdd(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - - exists, err = client.BFExists(ctx, "testbf1", "item1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - }) - - It("should BFInfo and BFReserve", Label("bloom", "bfinfo", "bfreserve"), func() { - err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - }) - - It("should BFInfoCapacity, BFInfoSize, BFInfoFilters, BFInfoItems, BFInfoExpansion, ", Label("bloom", "bfinfocapacity", "bfinfosize", "bfinfofilters", "bfinfoitems", "bfinfoexpansion"), func() { - err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfoCapacity(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - - result, err = client.BFInfoItems(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.ItemsInserted).To(BeEquivalentTo(int64(0))) - - result, err = client.BFInfoSize(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Size).To(BeEquivalentTo(int64(4056))) - - err = client.BFReserveExpansion(ctx, "testbf2", 0.001, 2000, 3).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.BFInfoFilters(ctx, "testbf2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.Filters).To(BeEquivalentTo(int64(1))) - - result, err = client.BFInfoExpansion(ctx, "testbf2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFInsert", Label("bloom", "bfinsert"), func() { - options := &redis.BFInsertOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - NoCreate: true, - } - - resultInsert, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("ERR not found")) - - options = &redis.BFInsertOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - NoCreate: false, - } - - resultInsert, err = client.BFInsert(ctx, "testbf1", options, "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultInsert)).To(BeEquivalentTo(1)) - - exists, err := client.BFExists(ctx, "testbf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFMAdd", Label("bloom", "bfmadd"), func() { - resultAdd, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultAdd)).To(Equal(3)) - - resultInfo, err := client.BFInfo(ctx, "testbf1").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(3))) - resultAdd2, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultAdd2[0]).To(BeFalse()) - Expect(resultAdd2[1]).To(BeFalse()) - Expect(resultAdd2[2]).To(BeTrue()) - }) - - It("should BFMExists", Label("bloom", "bfmexists"), func() { - exist, err := client.BFMExists(ctx, "testbf1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(exist)).To(Equal(3)) - Expect(exist[0]).To(BeFalse()) - Expect(exist[1]).To(BeFalse()) - Expect(exist[2]).To(BeFalse()) - - _, err = client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - - exist, err = client.BFMExists(ctx, "testbf1", "item1", "item2", "item3", "item4").Result() - - Expect(err).NotTo(HaveOccurred()) - Expect(len(exist)).To(Equal(4)) - Expect(exist[0]).To(BeTrue()) - Expect(exist[1]).To(BeTrue()) - Expect(exist[2]).To(BeTrue()) - Expect(exist[3]).To(BeFalse()) - }) - - It("should BFReserveExpansion", Label("bloom", "bfreserveexpansion"), func() { - err := client.BFReserveExpansion(ctx, "testbf1", 0.001, 2000, 3).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - - It("should BFReserveNonScaling", Label("bloom", "bfreservenonscaling"), func() { - err := client.BFReserveNonScaling(ctx, "testbfns1", 0.001, 1000).Err() - Expect(err).NotTo(HaveOccurred()) - - _, err = client.BFInfo(ctx, "testbfns1").Result() - Expect(err).To(HaveOccurred()) - }) - - It("should BFScanDump and BFLoadChunk", Label("bloom", "bfscandump", "bfloadchunk"), func() { - err := client.BFReserve(ctx, "testbfsd1", 0.001, 3000).Err() - Expect(err).NotTo(HaveOccurred()) - for i := 0; i < 1000; i++ { - client.BFAdd(ctx, "testbfsd1", i) - } - infBefore := client.BFInfoSize(ctx, "testbfsd1") - fd := []redis.ScanDump{} - sd, err := client.BFScanDump(ctx, "testbfsd1", 0).Result() - for { - if sd.Iter == 0 { - break - } - Expect(err).NotTo(HaveOccurred()) - fd = append(fd, sd) - sd, err = client.BFScanDump(ctx, "testbfsd1", sd.Iter).Result() - } - client.Del(ctx, "testbfsd1") - for _, e := range fd { - client.BFLoadChunk(ctx, "testbfsd1", e.Iter, e.Data) - } - infAfter := client.BFInfoSize(ctx, "testbfsd1") - Expect(infBefore).To(BeEquivalentTo(infAfter)) - }) - - It("should BFReserveWithArgs", Label("bloom", "bfreserveargs"), func() { - options := &redis.BFReserveOptions{ - Capacity: 2000, - Error: 0.001, - Expansion: 3, - NonScaling: false, - } - err := client.BFReserveWithArgs(ctx, "testbf", options).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.BFInfo(ctx, "testbf").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) - Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) - }) - }) - - Describe("cuckoo", Label("cuckoo"), func() { - It("should CFAdd", Label("cuckoo", "cfadd"), func() { - add, err := client.CFAdd(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(add).To(BeTrue()) - - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - info, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + setupRedisClient := func(protocolVersion int) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + DB: 0, + Protocol: protocolVersion, }) + } - It("should CFAddNX", Label("cuckoo", "cfaddnx"), func() { - add, err := client.CFAddNX(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(add).To(BeTrue()) + protocols := []int{2, 3} + for _, protocol := range protocols { + protocol := protocol // capture loop variable for each context - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) + Context(fmt.Sprintf("with protocol version %d", protocol), func() { + var client *redis.Client - result, err := client.CFAddNX(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeFalse()) + BeforeEach(func() { + client = setupRedisClient(protocol) + Expect(client.FlushAll(ctx).Err()).NotTo(HaveOccurred()) + }) - info, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) - }) - - It("should CFCount", Label("cuckoo", "cfcount"), func() { - err := client.CFAdd(ctx, "testcf1", "item1").Err() - cnt, err := client.CFCount(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cnt).To(BeEquivalentTo(int64(1))) - - err = client.CFAdd(ctx, "testcf1", "item1").Err() - Expect(err).NotTo(HaveOccurred()) - - cnt, err = client.CFCount(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(cnt).To(BeEquivalentTo(int64(2))) - }) - - It("should CFDel and CFExists", Label("cuckoo", "cfdel", "cfexists"), func() { - err := client.CFAdd(ctx, "testcf1", "item1").Err() - Expect(err).NotTo(HaveOccurred()) - - exists, err := client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - - del, err := client.CFDel(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(del).To(BeTrue()) - - exists, err = client.CFExists(ctx, "testcf1", "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should CFInfo and CFReserve", Label("cuckoo", "cfinfo", "cfreserve"), func() { - err := client.CFReserve(ctx, "testcf1", 1000).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveExpansion(ctx, "testcfe1", 1000, 1).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveBucketSize(ctx, "testcfbs1", 1000, 4).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CFReserveMaxIterations(ctx, "testcfmi1", 1000, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) - }) - - It("should CFScanDump and CFLoadChunk", Label("bloom", "cfscandump", "cfloadchunk"), func() { - err := client.CFReserve(ctx, "testcfsd1", 1000).Err() - Expect(err).NotTo(HaveOccurred()) - for i := 0; i < 1000; i++ { - Item := fmt.Sprintf("item%d", i) - client.CFAdd(ctx, "testcfsd1", Item) - } - infBefore := client.CFInfo(ctx, "testcfsd1") - fd := []redis.ScanDump{} - sd, err := client.CFScanDump(ctx, "testcfsd1", 0).Result() - for { - if sd.Iter == 0 { - break + AfterEach(func() { + if client != nil { + client.FlushDB(ctx) + client.Close() } - Expect(err).NotTo(HaveOccurred()) - fd = append(fd, sd) - sd, err = client.CFScanDump(ctx, "testcfsd1", sd.Iter).Result() - } - client.Del(ctx, "testcfsd1") - for _, e := range fd { - client.CFLoadChunk(ctx, "testcfsd1", e.Iter, e.Data) - } - infAfter := client.CFInfo(ctx, "testcfsd1") - Expect(infBefore).To(BeEquivalentTo(infAfter)) - }) - - It("should CFInfo and CFReserveWithArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { - args := &redis.CFReserveOptions{ - Capacity: 2048, - BucketSize: 3, - MaxIterations: 15, - Expansion: 2, - } - - err := client.CFReserveWithArgs(ctx, "testcf1", args).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFInfo(ctx, "testcf1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) - Expect(result.BucketSize).To(BeEquivalentTo(int64(3))) - Expect(result.MaxIteration).To(BeEquivalentTo(int64(15))) - Expect(result.ExpansionRate).To(BeEquivalentTo(int64(2))) - }) - - It("should CFInsert", Label("cuckoo", "cfinsert"), func() { - args := &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: true, - } - - result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() - Expect(err).To(HaveOccurred()) - - args = &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: false, - } - - result, err = client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - }) - - It("should CFInsertNX", Label("cuckoo", "cfinsertnx"), func() { - args := &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: true, - } - - result, err := client.CFInsertNX(ctx, "testcf1", args, "item1", "item2", "item2").Result() - Expect(err).To(HaveOccurred()) - - args = &redis.CFInsertOptions{ - Capacity: 3000, - NoCreate: false, - } - - result, err = client.CFInsertNX(ctx, "testcf2", args, "item1", "item2", "item2").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(1))) - Expect(result[2]).To(BeEquivalentTo(int64(0))) - }) - - It("should CFMexists", Label("cuckoo", "cfmexists"), func() { - err := client.CFInsert(ctx, "testcf1", nil, "item1", "item2", "item3").Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CFMExists(ctx, "testcf1", "item1", "item2", "item3", "item4").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(4)) - Expect(result[0]).To(BeTrue()) - Expect(result[1]).To(BeTrue()) - Expect(result[2]).To(BeTrue()) - Expect(result[3]).To(BeFalse()) - }) - }) - - Describe("CMS", Label("cms"), func() { - It("should CMSIncrBy", Label("cms", "cmsincrby"), func() { - err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CMSIncrBy(ctx, "testcms1", "item1", 1, "item2", 2, "item3", 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(2))) - Expect(result[2]).To(BeEquivalentTo(int64(3))) - }) - - It("should CMSInitByDim and CMSInfo", Label("cms", "cmsinitbydim", "cmsinfo"), func() { - err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.CMSInfo(ctx, "testcms1").Result() - Expect(err).NotTo(HaveOccurred()) - - Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) - Expect(info.Width).To(BeEquivalentTo(int64(5))) - Expect(info.Depth).To(BeEquivalentTo(int64(10))) - }) - - It("should CMSInitByProb", Label("cms", "cmsinitbyprob"), func() { - err := client.CMSInitByProb(ctx, "testcms1", 0.002, 0.01).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.CMSInfo(ctx, "testcms1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) - }) - - It("should CMSMerge, CMSMergeWithWeight and CMSQuery", Label("cms", "cmsmerge", "cmsquery", "NonRedisEnterprise"), func() { - err := client.CMSMerge(ctx, "destCms1", "testcms2", "testcms3").Err() - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("CMS: key does not exist")) - - err = client.CMSInitByDim(ctx, "destCms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "destCms2", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms1", 2, 20).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms2", 3, 20).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() - Expect(err).To(MatchError("CMS: width/depth is not equal")) - - client.Del(ctx, "cms1", "cms2") - - err = client.CMSInitByDim(ctx, "cms1", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - err = client.CMSInitByDim(ctx, "cms2", 5, 10).Err() - Expect(err).NotTo(HaveOccurred()) - - client.CMSIncrBy(ctx, "cms1", "item1", 1, "item2", 2) - client.CMSIncrBy(ctx, "cms2", "item2", 2, "item3", 3) - - err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() - Expect(err).NotTo(HaveOccurred()) - - result, err := client.CMSQuery(ctx, "destCms1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(4))) - Expect(result[2]).To(BeEquivalentTo(int64(3))) - - sourceSketches := map[string]int64{ - "cms1": 1, - "cms2": 2, - } - err = client.CMSMergeWithWeight(ctx, "destCms2", sourceSketches).Err() - Expect(err).NotTo(HaveOccurred()) - - result, err = client.CMSQuery(ctx, "destCms2", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(result)).To(BeEquivalentTo(3)) - Expect(result[0]).To(BeEquivalentTo(int64(1))) - Expect(result[1]).To(BeEquivalentTo(int64(6))) - Expect(result[2]).To(BeEquivalentTo(int64(6))) - }) - }) - - Describe("TopK", Label("topk"), func() { - It("should TopKReserve, TopKInfo, TopKAdd, TopKQuery, TopKCount, TopKIncrBy, TopKList, TopKListWithCount", Label("topk", "topkreserve", "topkinfo", "topkadd", "topkquery", "topkcount", "topkincrby", "topklist", "topklistwithcount"), func() { - err := client.TopKReserve(ctx, "topk1", 3).Err() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TopKInfo(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) - - resultAdd, err := client.TopKAdd(ctx, "topk1", "item1", "item2", 3, "item1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultAdd)).To(BeEquivalentTo(int64(4))) - - resultQuery, err := client.TopKQuery(ctx, "topk1", "item1", "item2", 4, 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultQuery)).To(BeEquivalentTo(4)) - Expect(resultQuery[0]).To(BeTrue()) - Expect(resultQuery[1]).To(BeTrue()) - Expect(resultQuery[2]).To(BeFalse()) - Expect(resultQuery[3]).To(BeTrue()) - - resultCount, err := client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultCount)).To(BeEquivalentTo(3)) - Expect(resultCount[0]).To(BeEquivalentTo(int64(2))) - Expect(resultCount[1]).To(BeEquivalentTo(int64(1))) - Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) - - resultIncr, err := client.TopKIncrBy(ctx, "topk1", "item1", 5, "item2", 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultIncr)).To(BeEquivalentTo(2)) - - resultCount, err = client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultCount)).To(BeEquivalentTo(3)) - Expect(resultCount[0]).To(BeEquivalentTo(int64(7))) - Expect(resultCount[1]).To(BeEquivalentTo(int64(11))) - Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) - - resultList, err := client.TopKList(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultList)).To(BeEquivalentTo(3)) - Expect(resultList).To(ContainElements("item2", "item1", "3")) - - resultListWithCount, err := client.TopKListWithCount(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(resultListWithCount)).To(BeEquivalentTo(3)) - Expect(resultListWithCount["3"]).To(BeEquivalentTo(int64(1))) - Expect(resultListWithCount["item1"]).To(BeEquivalentTo(int64(7))) - Expect(resultListWithCount["item2"]).To(BeEquivalentTo(int64(11))) - }) - - It("should TopKReserveWithOptions", Label("topk", "topkreservewithoptions"), func() { - err := client.TopKReserveWithOptions(ctx, "topk1", 3, 1500, 8, 0.5).Err() - Expect(err).NotTo(HaveOccurred()) - - resultInfo, err := client.TopKInfo(ctx, "topk1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) - Expect(resultInfo.Width).To(BeEquivalentTo(int64(1500))) - Expect(resultInfo.Depth).To(BeEquivalentTo(int64(8))) - Expect(resultInfo.Decay).To(BeEquivalentTo(0.5)) - }) - }) - - Describe("t-digest", Label("tdigest"), func() { - It("should TDigestAdd, TDigestCreate, TDigestInfo, TDigestByRank, TDigestByRevRank, TDigestCDF, TDigestMax, TDigestMin, TDigestQuantile, TDigestRank, TDigestRevRank, TDigestTrimmedMean, TDigestReset, ", Label("tdigest", "tdigestadd", "tdigestcreate", "tdigestinfo", "tdigestbyrank", "tdigestbyrevrank", "tdigestcdf", "tdigestmax", "tdigestmin", "tdigestquantile", "tdigestrank", "tdigestrevrank", "tdigesttrimmedmean", "tdigestreset"), func() { - err := client.TDigestCreate(ctx, "tdigest1").Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(0))) - - // Test with empty sketch - byRank, err := client.TDigestByRank(ctx, "tdigest1", 0, 1, 2, 3).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRank)).To(BeEquivalentTo(4)) - - byRevRank, err := client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRevRank)).To(BeEquivalentTo(3)) - - cdf, err := client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(cdf)).To(BeEquivalentTo(3)) - - max, err := client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(max)).To(BeTrue()) - - min, err := client.TDigestMin(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(min)).To(BeTrue()) - - quantile, err := client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(quantile)).To(BeEquivalentTo(2)) - - rank, err := client.TDigestRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rank)).To(BeEquivalentTo(2)) - - revRank, err := client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(revRank)).To(BeEquivalentTo(2)) - - trimmedMean, err := client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(math.IsNaN(trimmedMean)).To(BeTrue()) - - // Add elements - err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err = client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(10))) - - byRank, err = client.TDigestByRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRank)).To(BeEquivalentTo(3)) - Expect(byRank[0]).To(BeEquivalentTo(float64(10))) - Expect(byRank[1]).To(BeEquivalentTo(float64(20))) - Expect(byRank[2]).To(BeEquivalentTo(float64(30))) - - byRevRank, err = client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(byRevRank)).To(BeEquivalentTo(3)) - Expect(byRevRank[0]).To(BeEquivalentTo(float64(100))) - Expect(byRevRank[1]).To(BeEquivalentTo(float64(90))) - Expect(byRevRank[2]).To(BeEquivalentTo(float64(80))) - - cdf, err = client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(cdf)).To(BeEquivalentTo(3)) - Expect(cdf[0]).To(BeEquivalentTo(0.1)) - Expect(cdf[1]).To(BeEquivalentTo(0.3)) - Expect(cdf[2]).To(BeEquivalentTo(0.65)) - - max, err = client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(max).To(BeEquivalentTo(float64(100))) - - min, err = client.TDigestMin(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(min).To(BeEquivalentTo(float64(10))) - - quantile, err = client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(quantile)).To(BeEquivalentTo(2)) - Expect(quantile[0]).To(BeEquivalentTo(float64(20))) - Expect(quantile[1]).To(BeEquivalentTo(float64(30))) - - rank, err = client.TDigestRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(rank)).To(BeEquivalentTo(2)) - Expect(rank[0]).To(BeEquivalentTo(int64(0))) - Expect(rank[1]).To(BeEquivalentTo(int64(1))) - - revRank, err = client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(revRank)).To(BeEquivalentTo(2)) - Expect(revRank[0]).To(BeEquivalentTo(int64(9))) - Expect(revRank[1]).To(BeEquivalentTo(int64(8))) - - trimmedMean, err = client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(trimmedMean).To(BeEquivalentTo(float64(40))) - - reset, err := client.TDigestReset(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(reset).To(BeEquivalentTo("OK")) - }) - - It("should TDigestCreateWithCompression", Label("tdigest", "tcreatewithcompression"), func() { - err := client.TDigestCreateWithCompression(ctx, "tdigest1", 2000).Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Compression).To(BeEquivalentTo(int64(2000))) - }) - - It("should TDigestMerge", Label("tdigest", "tmerge", "NonRedisEnterprise"), func() { - err := client.TDigestCreate(ctx, "tdigest1").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.TDigestCreate(ctx, "tdigest2").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest2", 15, 25, 35, 45, 55, 65, 75, 85, 95, 105).Err() - Expect(err).NotTo(HaveOccurred()) - - err = client.TDigestCreate(ctx, "tdigest3").Err() - Expect(err).NotTo(HaveOccurred()) - err = client.TDigestAdd(ctx, "tdigest3", 50, 60, 70, 80, 90, 100, 110, 120, 130, 140).Err() - Expect(err).NotTo(HaveOccurred()) - - options := &redis.TDigestMergeOptions{ - Compression: 1000, - Override: false, - } - err = client.TDigestMerge(ctx, "tdigest1", options, "tdigest2", "tdigest3").Err() - Expect(err).NotTo(HaveOccurred()) - - info, err := client.TDigestInfo(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(info.Observations).To(BeEquivalentTo(int64(30))) - Expect(info.Compression).To(BeEquivalentTo(int64(1000))) - - max, err := client.TDigestMax(ctx, "tdigest1").Result() - Expect(err).NotTo(HaveOccurred()) - Expect(max).To(BeEquivalentTo(float64(140))) + }) + + Describe("bloom", Label("bloom"), func() { + It("should BFAdd", Label("bloom", "bfadd"), func() { + resultAdd, err := client.BFAdd(ctx, "testbf1", 1).Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd).To(BeTrue()) + + resultInfo, err := client.BFInfo(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should BFCard", Label("bloom", "bfcard"), func() { + // This is a probabilistic data structure, and it's not always guaranteed that we will get back + // the exact number of inserted items, during hash collisions + // But with such a low number of items (only 3), + // the probability of a collision is very low, so we can expect to get back the exact number of items + _, err := client.BFAdd(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.BFAdd(ctx, "testbf1", "item2").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.BFAdd(ctx, "testbf1", 3).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFCard(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEquivalentTo(int64(3))) + }) + + It("should BFExists", Label("bloom", "bfexists"), func() { + exists, err := client.BFExists(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) + + _, err = client.BFAdd(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + + exists, err = client.BFExists(ctx, "testbf1", "item1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + }) + + It("should BFInfo and BFReserve", Label("bloom", "bfinfo", "bfreserve"), func() { + err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + }) + + It("should BFInfoCapacity, BFInfoSize, BFInfoFilters, BFInfoItems, BFInfoExpansion, ", Label("bloom", "bfinfocapacity", "bfinfosize", "bfinfofilters", "bfinfoitems", "bfinfoexpansion"), func() { + err := client.BFReserve(ctx, "testbf1", 0.001, 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfoCapacity(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + + result, err = client.BFInfoItems(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.ItemsInserted).To(BeEquivalentTo(int64(0))) + + result, err = client.BFInfoSize(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Size).To(BeEquivalentTo(int64(4056))) + + err = client.BFReserveExpansion(ctx, "testbf2", 0.001, 2000, 3).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.BFInfoFilters(ctx, "testbf2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.Filters).To(BeEquivalentTo(int64(1))) + + result, err = client.BFInfoExpansion(ctx, "testbf2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFInsert", Label("bloom", "bfinsert"), func() { + options := &redis.BFInsertOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + NoCreate: true, + } + + _, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("ERR not found")) + + options = &redis.BFInsertOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + NoCreate: false, + } + + resultInsert, err := client.BFInsert(ctx, "testbf1", options, "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultInsert)).To(BeEquivalentTo(1)) + + exists, err := client.BFExists(ctx, "testbf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFMAdd", Label("bloom", "bfmadd"), func() { + resultAdd, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultAdd)).To(Equal(3)) + + resultInfo, err := client.BFInfo(ctx, "testbf1").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(resultInfo.ItemsInserted).To(BeEquivalentTo(int64(3))) + resultAdd2, err := client.BFMAdd(ctx, "testbf1", "item1", "item2", "item4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultAdd2[0]).To(BeFalse()) + Expect(resultAdd2[1]).To(BeFalse()) + Expect(resultAdd2[2]).To(BeTrue()) + }) + + It("should BFMExists", Label("bloom", "bfmexists"), func() { + exist, err := client.BFMExists(ctx, "testbf1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(exist)).To(Equal(3)) + Expect(exist[0]).To(BeFalse()) + Expect(exist[1]).To(BeFalse()) + Expect(exist[2]).To(BeFalse()) + + _, err = client.BFMAdd(ctx, "testbf1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + + exist, err = client.BFMExists(ctx, "testbf1", "item1", "item2", "item3", "item4").Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(len(exist)).To(Equal(4)) + Expect(exist[0]).To(BeTrue()) + Expect(exist[1]).To(BeTrue()) + Expect(exist[2]).To(BeTrue()) + Expect(exist[3]).To(BeFalse()) + }) + + It("should BFReserveExpansion", Label("bloom", "bfreserveexpansion"), func() { + err := client.BFReserveExpansion(ctx, "testbf1", 0.001, 2000, 3).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + + It("should BFReserveNonScaling", Label("bloom", "bfreservenonscaling"), func() { + err := client.BFReserveNonScaling(ctx, "testbfns1", 0.001, 1000).Err() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.BFInfo(ctx, "testbfns1").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should BFScanDump and BFLoadChunk", Label("bloom", "bfscandump", "bfloadchunk"), func() { + err := client.BFReserve(ctx, "testbfsd1", 0.001, 3000).Err() + Expect(err).NotTo(HaveOccurred()) + for i := 0; i < 1000; i++ { + client.BFAdd(ctx, "testbfsd1", i) + } + infBefore := client.BFInfoSize(ctx, "testbfsd1") + fd := []redis.ScanDump{} + sd, err := client.BFScanDump(ctx, "testbfsd1", 0).Result() + for { + if sd.Iter == 0 { + break + } + Expect(err).NotTo(HaveOccurred()) + fd = append(fd, sd) + sd, err = client.BFScanDump(ctx, "testbfsd1", sd.Iter).Result() + } + client.Del(ctx, "testbfsd1") + for _, e := range fd { + client.BFLoadChunk(ctx, "testbfsd1", e.Iter, e.Data) + } + infAfter := client.BFInfoSize(ctx, "testbfsd1") + Expect(infBefore).To(BeEquivalentTo(infAfter)) + }) + + It("should BFReserveWithArgs", Label("bloom", "bfreserveargs"), func() { + options := &redis.BFReserveOptions{ + Capacity: 2000, + Error: 0.001, + Expansion: 3, + NonScaling: false, + } + err := client.BFReserveWithArgs(ctx, "testbf", options).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.BFInfo(ctx, "testbf").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.BFInfo{})) + Expect(result.Capacity).To(BeEquivalentTo(int64(2000))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(3))) + }) + }) + + Describe("cuckoo", Label("cuckoo"), func() { + It("should CFAdd", Label("cuckoo", "cfadd"), func() { + add, err := client.CFAdd(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(BeTrue()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + info, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should CFAddNX", Label("cuckoo", "cfaddnx"), func() { + add, err := client.CFAddNX(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(BeTrue()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + result, err := client.CFAddNX(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeFalse()) + + info, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(info.NumItemsInserted).To(BeEquivalentTo(int64(1))) + }) + + It("should CFCount", Label("cuckoo", "cfcount"), func() { + err := client.CFAdd(ctx, "testcf1", "item1").Err() + cnt, err := client.CFCount(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cnt).To(BeEquivalentTo(int64(1))) + + err = client.CFAdd(ctx, "testcf1", "item1").Err() + Expect(err).NotTo(HaveOccurred()) + + cnt, err = client.CFCount(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(cnt).To(BeEquivalentTo(int64(2))) + }) + + It("should CFDel and CFExists", Label("cuckoo", "cfdel", "cfexists"), func() { + err := client.CFAdd(ctx, "testcf1", "item1").Err() + Expect(err).NotTo(HaveOccurred()) + + exists, err := client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeTrue()) + + del, err := client.CFDel(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(del).To(BeTrue()) + + exists, err = client.CFExists(ctx, "testcf1", "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(BeFalse()) + }) + + It("should CFInfo and CFReserve", Label("cuckoo", "cfinfo", "cfreserve"), func() { + err := client.CFReserve(ctx, "testcf1", 1000).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveExpansion(ctx, "testcfe1", 1000, 1).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveBucketSize(ctx, "testcfbs1", 1000, 4).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CFReserveMaxIterations(ctx, "testcfmi1", 1000, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) + }) + + It("should CFScanDump and CFLoadChunk", Label("bloom", "cfscandump", "cfloadchunk"), func() { + err := client.CFReserve(ctx, "testcfsd1", 1000).Err() + Expect(err).NotTo(HaveOccurred()) + for i := 0; i < 1000; i++ { + Item := fmt.Sprintf("item%d", i) + client.CFAdd(ctx, "testcfsd1", Item) + } + infBefore := client.CFInfo(ctx, "testcfsd1") + fd := []redis.ScanDump{} + sd, err := client.CFScanDump(ctx, "testcfsd1", 0).Result() + for { + if sd.Iter == 0 { + break + } + Expect(err).NotTo(HaveOccurred()) + fd = append(fd, sd) + sd, err = client.CFScanDump(ctx, "testcfsd1", sd.Iter).Result() + } + client.Del(ctx, "testcfsd1") + for _, e := range fd { + client.CFLoadChunk(ctx, "testcfsd1", e.Iter, e.Data) + } + infAfter := client.CFInfo(ctx, "testcfsd1") + Expect(infBefore).To(BeEquivalentTo(infAfter)) + }) + + It("should CFInfo and CFReserveWithArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { + args := &redis.CFReserveOptions{ + Capacity: 2048, + BucketSize: 3, + MaxIterations: 15, + Expansion: 2, + } + + err := client.CFReserveWithArgs(ctx, "testcf1", args).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFInfo(ctx, "testcf1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeAssignableToTypeOf(redis.CFInfo{})) + Expect(result.BucketSize).To(BeEquivalentTo(int64(3))) + Expect(result.MaxIteration).To(BeEquivalentTo(int64(15))) + Expect(result.ExpansionRate).To(BeEquivalentTo(int64(2))) + }) + + It("should CFInsert", Label("cuckoo", "cfinsert"), func() { + args := &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: true, + } + + result, err := client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + Expect(err).To(HaveOccurred()) + + args = &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: false, + } + + result, err = client.CFInsert(ctx, "testcf1", args, "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + }) + + It("should CFInsertNX", Label("cuckoo", "cfinsertnx"), func() { + args := &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: true, + } + + _, err := client.CFInsertNX(ctx, "testcf1", args, "item1", "item2", "item2").Result() + Expect(err).To(HaveOccurred()) + + args = &redis.CFInsertOptions{ + Capacity: 3000, + NoCreate: false, + } + + result, err := client.CFInsertNX(ctx, "testcf2", args, "item1", "item2", "item2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(1))) + Expect(result[2]).To(BeEquivalentTo(int64(0))) + }) + + It("should CFMexists", Label("cuckoo", "cfmexists"), func() { + err := client.CFInsert(ctx, "testcf1", nil, "item1", "item2", "item3").Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CFMExists(ctx, "testcf1", "item1", "item2", "item3", "item4").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(4)) + Expect(result[0]).To(BeTrue()) + Expect(result[1]).To(BeTrue()) + Expect(result[2]).To(BeTrue()) + Expect(result[3]).To(BeFalse()) + }) + }) + + Describe("CMS", Label("cms"), func() { + It("should CMSIncrBy", Label("cms", "cmsincrby"), func() { + err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CMSIncrBy(ctx, "testcms1", "item1", 1, "item2", 2, "item3", 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(2))) + Expect(result[2]).To(BeEquivalentTo(int64(3))) + }) + + It("should CMSInitByDim and CMSInfo", Label("cms", "cmsinitbydim", "cmsinfo"), func() { + err := client.CMSInitByDim(ctx, "testcms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.CMSInfo(ctx, "testcms1").Result() + Expect(err).NotTo(HaveOccurred()) + + Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) + Expect(info.Width).To(BeEquivalentTo(int64(5))) + Expect(info.Depth).To(BeEquivalentTo(int64(10))) + }) + + It("should CMSInitByProb", Label("cms", "cmsinitbyprob"), func() { + err := client.CMSInitByProb(ctx, "testcms1", 0.002, 0.01).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.CMSInfo(ctx, "testcms1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info).To(BeAssignableToTypeOf(redis.CMSInfo{})) + }) + + It("should CMSMerge, CMSMergeWithWeight and CMSQuery", Label("cms", "cmsmerge", "cmsquery", "NonRedisEnterprise"), func() { + err := client.CMSMerge(ctx, "destCms1", "testcms2", "testcms3").Err() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("CMS: key does not exist")) + + err = client.CMSInitByDim(ctx, "destCms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "destCms2", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms1", 2, 20).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms2", 3, 20).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() + Expect(err).To(MatchError("CMS: width/depth is not equal")) + + client.Del(ctx, "cms1", "cms2") + + err = client.CMSInitByDim(ctx, "cms1", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.CMSInitByDim(ctx, "cms2", 5, 10).Err() + Expect(err).NotTo(HaveOccurred()) + + client.CMSIncrBy(ctx, "cms1", "item1", 1, "item2", 2) + client.CMSIncrBy(ctx, "cms2", "item2", 2, "item3", 3) + + err = client.CMSMerge(ctx, "destCms1", "cms1", "cms2").Err() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.CMSQuery(ctx, "destCms1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(4))) + Expect(result[2]).To(BeEquivalentTo(int64(3))) + + sourceSketches := map[string]int64{ + "cms1": 1, + "cms2": 2, + } + err = client.CMSMergeWithWeight(ctx, "destCms2", sourceSketches).Err() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.CMSQuery(ctx, "destCms2", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(result)).To(BeEquivalentTo(3)) + Expect(result[0]).To(BeEquivalentTo(int64(1))) + Expect(result[1]).To(BeEquivalentTo(int64(6))) + Expect(result[2]).To(BeEquivalentTo(int64(6))) + }) + }) + + Describe("TopK", Label("topk"), func() { + It("should TopKReserve, TopKInfo, TopKAdd, TopKQuery, TopKCount, TopKIncrBy, TopKList, TopKListWithCount", Label("topk", "topkreserve", "topkinfo", "topkadd", "topkquery", "topkcount", "topkincrby", "topklist", "topklistwithcount"), func() { + err := client.TopKReserve(ctx, "topk1", 3).Err() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TopKInfo(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) + + resultAdd, err := client.TopKAdd(ctx, "topk1", "item1", "item2", 3, "item1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultAdd)).To(BeEquivalentTo(int64(4))) + + resultQuery, err := client.TopKQuery(ctx, "topk1", "item1", "item2", 4, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultQuery)).To(BeEquivalentTo(4)) + Expect(resultQuery[0]).To(BeTrue()) + Expect(resultQuery[1]).To(BeTrue()) + Expect(resultQuery[2]).To(BeFalse()) + Expect(resultQuery[3]).To(BeTrue()) + + resultCount, err := client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultCount)).To(BeEquivalentTo(3)) + Expect(resultCount[0]).To(BeEquivalentTo(int64(2))) + Expect(resultCount[1]).To(BeEquivalentTo(int64(1))) + Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) + + resultIncr, err := client.TopKIncrBy(ctx, "topk1", "item1", 5, "item2", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultIncr)).To(BeEquivalentTo(2)) + + resultCount, err = client.TopKCount(ctx, "topk1", "item1", "item2", "item3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultCount)).To(BeEquivalentTo(3)) + Expect(resultCount[0]).To(BeEquivalentTo(int64(7))) + Expect(resultCount[1]).To(BeEquivalentTo(int64(11))) + Expect(resultCount[2]).To(BeEquivalentTo(int64(0))) + + resultList, err := client.TopKList(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultList)).To(BeEquivalentTo(3)) + Expect(resultList).To(ContainElements("item2", "item1", "3")) + + resultListWithCount, err := client.TopKListWithCount(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(resultListWithCount)).To(BeEquivalentTo(3)) + Expect(resultListWithCount["3"]).To(BeEquivalentTo(int64(1))) + Expect(resultListWithCount["item1"]).To(BeEquivalentTo(int64(7))) + Expect(resultListWithCount["item2"]).To(BeEquivalentTo(int64(11))) + }) + + It("should TopKReserveWithOptions", Label("topk", "topkreservewithoptions"), func() { + err := client.TopKReserveWithOptions(ctx, "topk1", 3, 1500, 8, 0.5).Err() + Expect(err).NotTo(HaveOccurred()) + + resultInfo, err := client.TopKInfo(ctx, "topk1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resultInfo.K).To(BeEquivalentTo(int64(3))) + Expect(resultInfo.Width).To(BeEquivalentTo(int64(1500))) + Expect(resultInfo.Depth).To(BeEquivalentTo(int64(8))) + Expect(resultInfo.Decay).To(BeEquivalentTo(0.5)) + }) + }) + + Describe("t-digest", Label("tdigest"), func() { + It("should TDigestAdd, TDigestCreate, TDigestInfo, TDigestByRank, TDigestByRevRank, TDigestCDF, TDigestMax, TDigestMin, TDigestQuantile, TDigestRank, TDigestRevRank, TDigestTrimmedMean, TDigestReset, ", Label("tdigest", "tdigestadd", "tdigestcreate", "tdigestinfo", "tdigestbyrank", "tdigestbyrevrank", "tdigestcdf", "tdigestmax", "tdigestmin", "tdigestquantile", "tdigestrank", "tdigestrevrank", "tdigesttrimmedmean", "tdigestreset"), func() { + err := client.TDigestCreate(ctx, "tdigest1").Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(0))) + + // Test with empty sketch + byRank, err := client.TDigestByRank(ctx, "tdigest1", 0, 1, 2, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRank)).To(BeEquivalentTo(4)) + + byRevRank, err := client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRevRank)).To(BeEquivalentTo(3)) + + cdf, err := client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(cdf)).To(BeEquivalentTo(3)) + + max, err := client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(max)).To(BeTrue()) + + min, err := client.TDigestMin(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(min)).To(BeTrue()) + + quantile, err := client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(quantile)).To(BeEquivalentTo(2)) + + rank, err := client.TDigestRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rank)).To(BeEquivalentTo(2)) + + revRank, err := client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(revRank)).To(BeEquivalentTo(2)) + + trimmedMean, err := client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(math.IsNaN(trimmedMean)).To(BeTrue()) + + // Add elements + err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err = client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(10))) + + byRank, err = client.TDigestByRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRank)).To(BeEquivalentTo(3)) + Expect(byRank[0]).To(BeEquivalentTo(float64(10))) + Expect(byRank[1]).To(BeEquivalentTo(float64(20))) + Expect(byRank[2]).To(BeEquivalentTo(float64(30))) + + byRevRank, err = client.TDigestByRevRank(ctx, "tdigest1", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(byRevRank)).To(BeEquivalentTo(3)) + Expect(byRevRank[0]).To(BeEquivalentTo(float64(100))) + Expect(byRevRank[1]).To(BeEquivalentTo(float64(90))) + Expect(byRevRank[2]).To(BeEquivalentTo(float64(80))) + + cdf, err = client.TDigestCDF(ctx, "tdigest1", 15, 35, 70).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(cdf)).To(BeEquivalentTo(3)) + Expect(cdf[0]).To(BeEquivalentTo(0.1)) + Expect(cdf[1]).To(BeEquivalentTo(0.3)) + Expect(cdf[2]).To(BeEquivalentTo(0.65)) + + max, err = client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(max).To(BeEquivalentTo(float64(100))) + + min, err = client.TDigestMin(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(min).To(BeEquivalentTo(float64(10))) + + quantile, err = client.TDigestQuantile(ctx, "tdigest1", 0.1, 0.2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(quantile)).To(BeEquivalentTo(2)) + Expect(quantile[0]).To(BeEquivalentTo(float64(20))) + Expect(quantile[1]).To(BeEquivalentTo(float64(30))) + + rank, err = client.TDigestRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(rank)).To(BeEquivalentTo(2)) + Expect(rank[0]).To(BeEquivalentTo(int64(0))) + Expect(rank[1]).To(BeEquivalentTo(int64(1))) + + revRank, err = client.TDigestRevRank(ctx, "tdigest1", 10, 20).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(revRank)).To(BeEquivalentTo(2)) + Expect(revRank[0]).To(BeEquivalentTo(int64(9))) + Expect(revRank[1]).To(BeEquivalentTo(int64(8))) + + trimmedMean, err = client.TDigestTrimmedMean(ctx, "tdigest1", 0.1, 0.6).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(trimmedMean).To(BeEquivalentTo(float64(40))) + + reset, err := client.TDigestReset(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(reset).To(BeEquivalentTo("OK")) + }) + + It("should TDigestCreateWithCompression", Label("tdigest", "tcreatewithcompression"), func() { + err := client.TDigestCreateWithCompression(ctx, "tdigest1", 2000).Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Compression).To(BeEquivalentTo(int64(2000))) + }) + + It("should TDigestMerge", Label("tdigest", "tmerge", "NonRedisEnterprise"), func() { + err := client.TDigestCreate(ctx, "tdigest1").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest1", 10, 20, 30, 40, 50, 60, 70, 80, 90, 100).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.TDigestCreate(ctx, "tdigest2").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest2", 15, 25, 35, 45, 55, 65, 75, 85, 95, 105).Err() + Expect(err).NotTo(HaveOccurred()) + + err = client.TDigestCreate(ctx, "tdigest3").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.TDigestAdd(ctx, "tdigest3", 50, 60, 70, 80, 90, 100, 110, 120, 130, 140).Err() + Expect(err).NotTo(HaveOccurred()) + + options := &redis.TDigestMergeOptions{ + Compression: 1000, + Override: false, + } + err = client.TDigestMerge(ctx, "tdigest1", options, "tdigest2", "tdigest3").Err() + Expect(err).NotTo(HaveOccurred()) + + info, err := client.TDigestInfo(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info.Observations).To(BeEquivalentTo(int64(30))) + Expect(info.Compression).To(BeEquivalentTo(int64(1000))) + + max, err := client.TDigestMax(ctx, "tdigest1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(max).To(BeEquivalentTo(float64(140))) + }) + }) }) - }) + } }) From 930d904205691ff06104fcc3ac108177077def35 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:20:59 +0200 Subject: [PATCH 24/68] Add guidance on unstable RESP3 support for RediSearch commands to README (#3177) * Add UnstableResp3 to docs * Add RawVal and RawResult to wordlist * Explain more about SetVal * Add UnstableResp to wordlist --- .github/wordlist.txt | 3 +++ README.md | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index c200c60b4..1fc34f733 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -54,6 +54,7 @@ stunnel SynDump TCP TLS +UnstableResp uri URI url @@ -62,3 +63,5 @@ RedisStack RedisGears RedisTimeseries RediSearch +RawResult +RawVal \ No newline at end of file diff --git a/README.md b/README.md index 37714a979..e71367659 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,21 @@ rdb := redis.NewClient(&redis.Options{ #### Unstable RESP3 Structures for RediSearch Commands When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. +To enable unstable RESP3, set the option in your client configuration: + +```go +redis.NewClient(&redis.Options{ + UnstableResp3: true, + }) +``` +**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data. + Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them: + +```go +res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult() +val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal() +``` + ## Contributing Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! From 080e051124d5e35a0d0c49e03f8547e86365c5d8 Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Wed, 20 Nov 2024 03:38:06 -0800 Subject: [PATCH 25/68] Eliminate redundant dial mutex causing unbounded connection queue contention (#3088) * Eliminate redundant dial mutex causing unbounded connection queue contention * Dialer connection timeouts unit test --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- redis.go | 2 -- redis_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/redis.go b/redis.go index c8b500809..2f576bdbe 100644 --- a/redis.go +++ b/redis.go @@ -176,8 +176,6 @@ func (hs *hooksMixin) withProcessPipelineHook( } func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) { - hs.hooksMu.Lock() - defer hs.hooksMu.Unlock() return hs.current.dial(ctx, network, addr) } diff --git a/redis_test.go b/redis_test.go index ef2125452..b5cf2570f 100644 --- a/redis_test.go +++ b/redis_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net" + "sync" "testing" "time" @@ -633,3 +634,67 @@ var _ = Describe("Hook with MinIdleConns", func() { })) }) }) + +var _ = Describe("Dialer connection timeouts", func() { + var client *redis.Client + + const dialSimulatedDelay = 1 * time.Second + + BeforeEach(func() { + options := redisOptions() + options.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { + // Simulated slow dialer. + // Note that the following sleep is deliberately not context-aware. + time.Sleep(dialSimulatedDelay) + return net.Dial("tcp", options.Addr) + } + options.MinIdleConns = 1 + client = redis.NewClient(options) + }) + + AfterEach(func() { + err := client.Close() + Expect(err).NotTo(HaveOccurred()) + }) + + It("does not contend on connection dial for concurrent commands", func() { + var wg sync.WaitGroup + + const concurrency = 10 + + durations := make(chan time.Duration, concurrency) + errs := make(chan error, concurrency) + + start := time.Now() + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + + start := time.Now() + err := client.Ping(ctx).Err() + durations <- time.Since(start) + errs <- err + }() + } + + wg.Wait() + close(durations) + close(errs) + + // All commands should eventually succeed, after acquiring a connection. + for err := range errs { + Expect(err).NotTo(HaveOccurred()) + } + + // Each individual command should complete within the simulated dial duration bound. + for duration := range durations { + Expect(duration).To(BeNumerically("<", 2*dialSimulatedDelay)) + } + + // Due to concurrent execution, the entire test suite should also complete within + // the same dial duration bound applied for individual commands. + Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay)) + }) +}) From f1ffb55c9aeea7dceab2bcf4970f168dbcf5a484 Mon Sep 17 00:00:00 2001 From: Justin <8886628+justinmir@users.noreply.github.com> Date: Wed, 20 Nov 2024 06:36:39 -0600 Subject: [PATCH 26/68] Only check latencies once every 10 seconds with `routeByLatency` (#2795) * Only check latencies once every 10 seconds with `routeByLatency` `routeByLatency` currently checks latencies any time a server returns a MOVED or READONLY reply. When a shard is down, the ClusterClient chooses to issue the request to a random server, which returns a MOVED reply. This causes a state refresh and a latency update on all servers. This can lead to significant ping load to clusters with a large number of clients. This introduces logic to ping only once every 10 seconds, only performing a latency update on a node during the `GC` function if the latency was set later than 10 seconds ago. Fixes https://github.com/redis/go-redis/issues/2782 * use UnixNano instead of Unix for better precision --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- osscluster.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osscluster.go b/osscluster.go index ce258ff36..72e922a80 100644 --- a/osscluster.go +++ b/osscluster.go @@ -21,6 +21,10 @@ import ( "github.com/redis/go-redis/v9/internal/rand" ) +const ( + minLatencyMeasurementInterval = 10 * time.Second +) + var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes") // ClusterOptions are used to configure a cluster client and should be @@ -316,6 +320,10 @@ type clusterNode struct { latency uint32 // atomic generation uint32 // atomic failing uint32 // atomic + + // last time the latency measurement was performed for the node, stored in nanoseconds + // from epoch + lastLatencyMeasurement int64 // atomic } func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode { @@ -368,6 +376,7 @@ func (n *clusterNode) updateLatency() { latency = float64(dur) / float64(successes) } atomic.StoreUint32(&n.latency, uint32(latency+0.5)) + n.SetLastLatencyMeasurement(time.Now()) } func (n *clusterNode) Latency() time.Duration { @@ -397,6 +406,10 @@ func (n *clusterNode) Generation() uint32 { return atomic.LoadUint32(&n.generation) } +func (n *clusterNode) LastLatencyMeasurement() int64 { + return atomic.LoadInt64(&n.lastLatencyMeasurement) +} + func (n *clusterNode) SetGeneration(gen uint32) { for { v := atomic.LoadUint32(&n.generation) @@ -406,6 +419,15 @@ func (n *clusterNode) SetGeneration(gen uint32) { } } +func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) { + for { + v := atomic.LoadInt64(&n.lastLatencyMeasurement) + if t.UnixNano() < v || atomic.CompareAndSwapInt64(&n.lastLatencyMeasurement, v, t.UnixNano()) { + break + } + } +} + //------------------------------------------------------------------------------ type clusterNodes struct { @@ -493,10 +515,11 @@ func (c *clusterNodes) GC(generation uint32) { c.mu.Lock() c.activeAddrs = c.activeAddrs[:0] + now := time.Now() for addr, node := range c.nodes { if node.Generation() >= generation { c.activeAddrs = append(c.activeAddrs, addr) - if c.opt.RouteByLatency { + if c.opt.RouteByLatency && node.LastLatencyMeasurement() < now.Add(-minLatencyMeasurementInterval).UnixNano() { go node.updateLatency() } continue From fc32d0a01d447fcdcfb90dd33a044f3661b9ccdf Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Thu, 21 Nov 2024 04:38:11 -0800 Subject: [PATCH 27/68] Recognize byte slice for key argument in cluster client hash slot computation (#3049) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- command.go | 2 ++ osscluster_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/command.go b/command.go index 7ea7862d5..3cb9538a5 100644 --- a/command.go +++ b/command.go @@ -167,6 +167,8 @@ func (cmd *baseCmd) stringArg(pos int) string { switch v := arg.(type) { case string: return v + case []byte: + return string(v) default: // TODO: consider using appendArg return fmt.Sprint(v) diff --git a/osscluster_test.go b/osscluster_test.go index f7bd1683f..9c3eaba35 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -653,6 +653,32 @@ var _ = Describe("ClusterClient", func() { Expect(client.Close()).NotTo(HaveOccurred()) }) + It("determines hash slots correctly for generic commands", func() { + opt := redisClusterOptions() + opt.MaxRedirects = -1 + client := cluster.newClusterClient(ctx, opt) + + err := client.Do(ctx, "GET", "A").Err() + Expect(err).To(Equal(redis.Nil)) + + err = client.Do(ctx, []byte("GET"), []byte("A")).Err() + Expect(err).To(Equal(redis.Nil)) + + Eventually(func() error { + return client.SwapNodes(ctx, "A") + }, 30*time.Second).ShouldNot(HaveOccurred()) + + err = client.Do(ctx, "GET", "A").Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MOVED")) + + err = client.Do(ctx, []byte("GET"), []byte("A")).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MOVED")) + + Expect(client.Close()).NotTo(HaveOccurred()) + }) + It("follows node redirection immediately", func() { // Configure retry backoffs far in excess of the expected duration of redirection opt := redisClusterOptions() From e63669e1706936ac794277340c51a51c5facca70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:38:38 +0200 Subject: [PATCH 28/68] chore(deps): bump rojopolis/spellcheck-github-actions (#3188) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.40.0 to 0.45.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.40.0...0.45.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index cc6d828c9..977f8c5c1 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.40.0 + uses: rojopolis/spellcheck-github-actions@0.45.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 73cc5f7c215f6f2079608fdbf32420f28ee36347 Mon Sep 17 00:00:00 2001 From: Cgol9 Date: Thu, 5 Dec 2024 01:10:04 -0700 Subject: [PATCH 29/68] SortByWithCount FTSearchOptions fix (#3201) * SortByWithCount FTSearchOptions fix * FTSearch test fix * Another FTSearch test fix * Another FTSearch test fix --------- Co-authored-by: Christopher Golling --- search_commands.go | 2 +- search_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/search_commands.go b/search_commands.go index e4df0b6fc..ede084e4e 100644 --- a/search_commands.go +++ b/search_commands.go @@ -1775,7 +1775,7 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { } } if options.SortByWithCount { - queryArgs = append(queryArgs, "WITHCOUT") + queryArgs = append(queryArgs, "WITHCOUNT") } } if options.LimitOffset >= 0 && options.Limit > 0 { diff --git a/search_test.go b/search_test.go index 48b9aa39b..e267c8ae8 100644 --- a/search_test.go +++ b/search_test.go @@ -125,6 +125,10 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res2.Docs[1].ID).To(BeEquivalentTo("doc2")) Expect(res2.Docs[0].ID).To(BeEquivalentTo("doc3")) + res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3.Total).To(BeEquivalentTo(int64(0))) + }) It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { From caa2592db731428e542670e8e6d3ccfa60603e8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:11:12 +0200 Subject: [PATCH 30/68] chore(deps): bump codecov/codecov-action from 4 to 5 (#3196) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5007423a4..c1d04b820 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: run: make test - name: Upload to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: coverage.txt token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file From 91dddc2e1108c779e8c5b85fd667029873c95172 Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:04:39 +0200 Subject: [PATCH 31/68] Test against Redis CE (#3191) * Create workflow that tests go-redis against docker * Add docker compose file * Add docker compose file * Change command in docker compose * Load modules locally * test varios redis versions * add env var to test-redis-enterprise action * cleaning code * cleaning code --- .github/workflows/build.yml | 45 ++++++++++++++++++++- .github/workflows/test-redis-enterprise.yml | 1 + docker-compose.yml | 21 ++++++++++ main_test.go | 5 ++- 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1d04b820..7578e962e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,4 +42,47 @@ jobs: uses: codecov/codecov-action@v5 with: files: coverage.txt - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + + test-redis-ce: + name: test-redis-ce + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + redis_version: + - "8.0-M01" + - "7.4.1" + - "7.2.6" + - "6.2.16" + go-version: + - "1.19.x" + - "1.20.x" + - "1.21.x" + + steps: + - name: Set up ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Docker Compose environment + - name: Set up Docker Compose environment + run: | + docker compose --profile all up -d + + - name: Run tests + env: + USE_CONTAINERIZED_REDIS: "true" + RE_CLUSTER: "true" + run: | + go test \ + --ginkgo.skip-file="ring_test.go" \ + --ginkgo.skip-file="sentinel_test.go" \ + --ginkgo.skip-file="osscluster_test.go" \ + --ginkgo.skip-file="pubsub_test.go" \ + --ginkgo.skip-file="gears_commands_test.go" \ + --ginkgo.label-filter='!NonRedisEnterprise' diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 940f0eae7..1cb36b8d2 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -47,6 +47,7 @@ jobs: - name: Test env: RE_CLUSTER: "1" + USE_CONTAINERIZED_REDIS: "1" run: | go test \ --ginkgo.skip-file="ring_test.go" \ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..a641e4d3b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +--- + +services: + + redis-stanalone: + image: redislabs/client-libs-test:8.0-M02 + container_name: redis-standalone + environment: + - REDIS_CLUSTER=no + - PORT=6379 + - TLS_PORT=6666 + command: --loadmodule /usr/local/lib/redis/modules/redisbloom.so --loadmodule /usr/local/lib/redis/modules/redisearch.so --loadmodule /usr/local/lib/redis/modules/redistimeseries.so --loadmodule /usr/local/lib/redis/modules/rejson.so + ports: + - 6379:6379 + - 6380:6379 + - 6666:6666 # TLS port + volumes: + - "./dockers/redis-standalone:/redis/work" + profiles: + - standalone + - all diff --git a/main_test.go b/main_test.go index 19e944446..44f8e6829 100644 --- a/main_test.go +++ b/main_test.go @@ -66,6 +66,7 @@ var cluster = &clusterScenario{ } var RECluster = false +var USE_CONTAINERIZED_REDIS = false func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -82,8 +83,8 @@ var _ = BeforeSuite(func() { } var err error RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) - - if !RECluster { + USE_CONTAINERIZED_REDIS, _ = strconv.ParseBool(os.Getenv("USE_CONTAINERIZED_REDIS")) + if !RECluster || !USE_CONTAINERIZED_REDIS { redisMain, err = startRedis(redisPort) Expect(err).NotTo(HaveOccurred()) From e953b76fdb62aa70b6f569d34c2af92b497f395a Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:24:47 +0200 Subject: [PATCH 32/68] Fix Redis CE tests (#3233) * Fix Redis CE tests * Remove manually modules installation --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a641e4d3b..f5ccb8f42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 - command: --loadmodule /usr/local/lib/redis/modules/redisbloom.so --loadmodule /usr/local/lib/redis/modules/redisearch.so --loadmodule /usr/local/lib/redis/modules/redistimeseries.so --loadmodule /usr/local/lib/redis/modules/rejson.so + ports: - 6379:6379 - 6380:6379 From f3a15c401e592eb029a24f121f7253a888262e95 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:29:51 +0000 Subject: [PATCH 33/68] DOC-4560 pipelines/transactions example (#3202) * DOC-4560 basic transaction example * DOC-4560 added pipe/transaction examples --- doctests/pipe_trans_example_test.go | 180 ++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 doctests/pipe_trans_example_test.go diff --git a/doctests/pipe_trans_example_test.go b/doctests/pipe_trans_example_test.go new file mode 100644 index 000000000..ea1dd5b48 --- /dev/null +++ b/doctests/pipe_trans_example_test.go @@ -0,0 +1,180 @@ +// EXAMPLE: pipe_trans_tutorial +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_transactions() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + // REMOVE_START + for i := 0; i < 5; i++ { + rdb.Del(ctx, fmt.Sprintf("seat:%d", i)) + } + + rdb.Del(ctx, "counter:1", "counter:2", "counter:3", "shellpath") + // REMOVE_END + + // STEP_START basic_pipe + pipe := rdb.Pipeline() + + for i := 0; i < 5; i++ { + pipe.Set(ctx, fmt.Sprintf("seat:%v", i), fmt.Sprintf("#%v", i), 0) + } + + cmds, err := pipe.Exec(ctx) + + if err != nil { + panic(err) + } + + for _, c := range cmds { + fmt.Printf("%v;", c.(*redis.StatusCmd).Val()) + } + + fmt.Println("") + // >>> OK;OK;OK;OK;OK; + + pipe = rdb.Pipeline() + + get0Result := pipe.Get(ctx, "seat:0") + get3Result := pipe.Get(ctx, "seat:3") + get4Result := pipe.Get(ctx, "seat:4") + + cmds, err = pipe.Exec(ctx) + + // The results are available only after the pipeline + // has finished executing. + fmt.Println(get0Result.Val()) // >>> #0 + fmt.Println(get3Result.Val()) // >>> #3 + fmt.Println(get4Result.Val()) // >>> #4 + // STEP_END + + // STEP_START basic_pipe_pipelined + var pd0Result *redis.StatusCmd + var pd3Result *redis.StatusCmd + var pd4Result *redis.StatusCmd + + cmds, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { + pd0Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:0")) + pd3Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:3")) + pd4Result = (*redis.StatusCmd)(pipe.Get(ctx, "seat:4")) + return nil + }) + + if err != nil { + panic(err) + } + + // The results are available only after the pipeline + // has finished executing. + fmt.Println(pd0Result.Val()) // >>> #0 + fmt.Println(pd3Result.Val()) // >>> #3 + fmt.Println(pd4Result.Val()) // >>> #4 + // STEP_END + + // STEP_START basic_trans + trans := rdb.TxPipeline() + + trans.IncrBy(ctx, "counter:1", 1) + trans.IncrBy(ctx, "counter:2", 2) + trans.IncrBy(ctx, "counter:3", 3) + + cmds, err = trans.Exec(ctx) + + for _, c := range cmds { + fmt.Println(c.(*redis.IntCmd).Val()) + } + // >>> 1 + // >>> 2 + // >>> 3 + // STEP_END + + // STEP_START basic_trans_txpipelined + var tx1Result *redis.IntCmd + var tx2Result *redis.IntCmd + var tx3Result *redis.IntCmd + + cmds, err = rdb.TxPipelined(ctx, func(trans redis.Pipeliner) error { + tx1Result = trans.IncrBy(ctx, "counter:1", 1) + tx2Result = trans.IncrBy(ctx, "counter:2", 2) + tx3Result = trans.IncrBy(ctx, "counter:3", 3) + return nil + }) + + if err != nil { + panic(err) + } + + fmt.Println(tx1Result.Val()) // >>> 2 + fmt.Println(tx2Result.Val()) // >>> 4 + fmt.Println(tx3Result.Val()) // >>> 6 + // STEP_END + + // STEP_START trans_watch + // Set initial value of `shellpath`. + rdb.Set(ctx, "shellpath", "/usr/syscmds/", 0) + + const maxRetries = 1000 + + // Retry if the key has been changed. + for i := 0; i < maxRetries; i++ { + err := rdb.Watch(ctx, + func(tx *redis.Tx) error { + currentPath, err := rdb.Get(ctx, "shellpath").Result() + newPath := currentPath + ":/usr/mycmds/" + + _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, "shellpath", newPath, 0) + return nil + }) + + return err + }, + "shellpath", + ) + + if err == nil { + // Success. + break + } else if err == redis.TxFailedErr { + // Optimistic lock lost. Retry the transaction. + continue + } else { + // Panic for any other error. + panic(err) + } + } + + fmt.Println(rdb.Get(ctx, "shellpath").Val()) + // >>> /usr/syscmds/:/usr/mycmds/ + // STEP_END + + // Output: + // OK;OK;OK;OK;OK; + // #0 + // #3 + // #4 + // #0 + // #3 + // #4 + // 1 + // 2 + // 3 + // 2 + // 4 + // 6 + // /usr/syscmds/:/usr/mycmds/ +} From 0e3ea5fd6bbd5b4e485bfb5256de82966ccb3c1e Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:02:55 +0000 Subject: [PATCH 34/68] DOC-4449 hash command examples (#3229) * DOC-4450 added hgetall and hvals doc examples * DOC-4449 added hgetall and hvals doc examples * DOC-4449 rewrote to avoid Collect and Keys functions (not available in test version of Go) * DOC-4449 replaced slices.Sort function with older alternative * DOC-4449 removed another instance of slices.Sort * DOC-4449 fixed bugs in tests * DOC-4449 try sort.Strings() for sorting key lists --------- Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- doctests/cmds_hash_test.go | 114 ++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/doctests/cmds_hash_test.go b/doctests/cmds_hash_test.go index f9630a9de..52ade74e9 100644 --- a/doctests/cmds_hash_test.go +++ b/doctests/cmds_hash_test.go @@ -5,6 +5,7 @@ package example_commands_test import ( "context" "fmt" + "sort" "github.com/redis/go-redis/v9" ) @@ -74,8 +75,20 @@ func ExampleClient_hset() { panic(err) } - fmt.Println(res6) - // >>> map[field1:Hello field2:Hi field3:World] + keys := make([]string, 0, len(res6)) + + for key, _ := range res6 { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + fmt.Printf("Key: %v, value: %v\n", key, res6[key]) + } + // >>> Key: field1, value: Hello + // >>> Key: field2, value: Hi + // >>> Key: field3, value: World // STEP_END // Output: @@ -84,7 +97,9 @@ func ExampleClient_hset() { // 2 // Hi // World - // map[field1:Hello field2:Hi field3:World] + // Key: field1, value: Hello + // Key: field2, value: Hi + // Key: field3, value: World } func ExampleClient_hget() { @@ -131,3 +146,96 @@ func ExampleClient_hget() { // foo // redis: nil } + +func ExampleClient_hgetall() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hgetall + hGetAllResult1, err := rdb.HSet(ctx, "myhash", + "field1", "Hello", + "field2", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(hGetAllResult1) // >>> 2 + + hGetAllResult2, err := rdb.HGetAll(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + keys := make([]string, 0, len(hGetAllResult2)) + + for key, _ := range hGetAllResult2 { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + fmt.Printf("Key: %v, value: %v\n", key, hGetAllResult2[key]) + } + // >>> Key: field1, value: Hello + // >>> Key: field2, value: World + // STEP_END + + // Output: + // 2 + // Key: field1, value: Hello + // Key: field2, value: World +} + +func ExampleClient_hvals() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hvals + hValsResult1, err := rdb.HSet(ctx, "myhash", + "field1", "Hello", + "field2", "World", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(hValsResult1) // >>> 2 + + hValsResult2, err := rdb.HVals(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + sort.Strings(hValsResult2) + + fmt.Println(hValsResult2) // >>> [Hello World] + // STEP_END + + // Output: + // 2 + // [Hello World] +} From efe0f65bf0dde8b04d6a94c84f21a7b3cba56303 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 20 Jan 2025 11:32:10 +0200 Subject: [PATCH 35/68] Order slices of strings to be sure what the output of Println in doctests will be. (#3241) * Sort the slices of strings in doctest to make the output deterministic * fix wording --- doctests/sets_example_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doctests/sets_example_test.go b/doctests/sets_example_test.go index 7446a2789..2d6504e2b 100644 --- a/doctests/sets_example_test.go +++ b/doctests/sets_example_test.go @@ -5,6 +5,7 @@ package example_commands_test import ( "context" "fmt" + "sort" "github.com/redis/go-redis/v9" ) @@ -215,6 +216,9 @@ func ExampleClient_saddsmembers() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res10) + fmt.Println(res10) // >>> [bike:1 bike:2 bike:3] // STEP_END @@ -294,6 +298,10 @@ func ExampleClient_sdiff() { panic(err) } + + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res13) + fmt.Println(res13) // >>> [bike:2 bike:3] // STEP_END @@ -349,6 +357,9 @@ func ExampleClient_multisets() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res15) + fmt.Println(res15) // >>> [bike:1 bike:2 bike:3 bike:4] res16, err := rdb.SDiff(ctx, "bikes:racing:france", "bikes:racing:usa", "bikes:racing:italy").Result() @@ -373,6 +384,9 @@ func ExampleClient_multisets() { panic(err) } + // Sort the strings in the slice to make sure the output is lexicographical + sort.Strings(res18) + fmt.Println(res18) // >>> [bike:2 bike:3] // STEP_END From 94b88f5ab2eee004aca71c34e342ba8d24763516 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Thu, 23 Jan 2025 14:47:28 +0200 Subject: [PATCH 36/68] fix(command): add missing `io-thread` key in `client info` (#3244) * Add 8.0m3 image in docker compose * Add new key `io-thread` in client info Redis 8.0 introduces new key `io-thread` in the response for client info. The key needs to be parsed. If an unknown key is observed, the client will return an error. * improve readibility * Revert "Add 8.0m3 image in docker compose" This reverts commit 787c41f42917fb7d3ca3471d9941304695a9b3c8. * add dockers directory to gitignore --- .gitignore | 3 ++- command.go | 3 +++ main_test.go | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6f868895b..7507584f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ +dockers/ *.rdb testdata/* .idea/ .DS_Store *.tar.gz -*.dic \ No newline at end of file +*.dic diff --git a/command.go b/command.go index 3cb9538a5..f5aad9149 100644 --- a/command.go +++ b/command.go @@ -5114,6 +5114,7 @@ type ClientInfo struct { OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full) OutputMemory int // omem, output buffer memory usage TotalMemory int // tot-mem, total memory consumed by this client in its various buffers + IoThread int // io-thread id Events string // file descriptor events (see below) LastCmd string // cmd, last command played User string // the authenticated username of the client @@ -5292,6 +5293,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.LibName = val case "lib-ver": info.LibVer = val + case "io-thread": + info.IoThread, err = strconv.Atoi(val) default: return nil, fmt.Errorf("redis: unexpected client info key(%s)", key) } diff --git a/main_test.go b/main_test.go index 44f8e6829..9f99b5c91 100644 --- a/main_test.go +++ b/main_test.go @@ -118,7 +118,8 @@ var _ = BeforeSuite(func() { sentinelSlave2Port, "--slaveof", "127.0.0.1", sentinelMasterPort) Expect(err).NotTo(HaveOccurred()) - Expect(startCluster(ctx, cluster)).NotTo(HaveOccurred()) + err = startCluster(ctx, cluster) + Expect(err).NotTo(HaveOccurred()) } else { redisPort = rediStackPort redisAddr = rediStackAddr From 1b4abd6dc69e4d86fb83167a0043aa57215cf3a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:56:20 +0200 Subject: [PATCH 37/68] chore(deps): bump golang.org/x/net in /example/otel (#3243) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.33.0. - [Commits](https://github.com/golang/net/compare/v0.23.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- example/otel/go.mod | 6 +++--- example/otel/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/example/otel/go.mod b/example/otel/go.mod index 4d97da4d1..3f1d858e1 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -34,9 +34,9 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index 5fb4c4588..e85481dbe 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -46,12 +46,12 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= From 9f9fa221a8dcd939c85ce82c58357ecb6d4d401e Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:57:14 +0000 Subject: [PATCH 38/68] DOC-4444 server management command examples (#3235) --- doctests/cmds_servermgmt_test.go | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 doctests/cmds_servermgmt_test.go diff --git a/doctests/cmds_servermgmt_test.go b/doctests/cmds_servermgmt_test.go new file mode 100644 index 000000000..8114abc1b --- /dev/null +++ b/doctests/cmds_servermgmt_test.go @@ -0,0 +1,74 @@ +// EXAMPLE: cmds_servermgmt +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_cmd_flushall() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // STEP_START flushall + // REMOVE_START + rdb.Set(ctx, "testkey1", "1", 0) + rdb.Set(ctx, "testkey2", "2", 0) + rdb.Set(ctx, "testkey3", "3", 0) + // REMOVE_END + flushAllResult1, err := rdb.FlushAll(ctx).Result() + + if err != nil { + panic(err) + } + + fmt.Println(flushAllResult1) // >>> OK + + flushAllResult2, err := rdb.Keys(ctx, "*").Result() + + if err != nil { + panic(err) + } + + fmt.Println(flushAllResult2) // >>> [] + // STEP_END + + // Output: + // OK + // [] +} + +func ExampleClient_cmd_info() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // STEP_START info + infoResult, err := rdb.Info(ctx).Result() + + if err != nil { + panic(err) + } + + // Check the first 8 characters (the full info string contains + // much more text than this). + fmt.Println(infoResult[:8]) // >>> # Server + // STEP_END + + // Output: + // # Server +} From 1139bc3aa9073851f67faa6d68df07a566901dd7 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 31 Jan 2025 16:14:11 +0200 Subject: [PATCH 39/68] fix(tests): enable testing with Redis CE 8.0-M4 in CI (#3247) * introduce github workflow for ci similar to the one in redis-py use prerelease for 8.0-M4 * Enable osscluster tests in CI * Add redis major version env Enable filtering test per redis major version Fix test for FT.SEARCH WITHSCORE, the default scorer has changed. fix Makefile syntax remove filter from github action fix makefile use the container name in Makefile * remove 1.20 from doctests * self review, cleanup, add comments * add comments, reorder prints, add default value for REDIS_MAJOR_VERSION --- .github/actions/run-tests/action.yml | 62 +++++++ .github/workflows/build.yml | 51 ++---- .github/workflows/doctests.yaml | 2 +- .github/workflows/test-redis-enterprise.yml | 6 +- .gitignore | 1 - Makefile | 5 +- bench_test.go | 2 +- commands_test.go | 1 - docker-compose.yml | 131 +++++++++++++- dockers/.gitignore | 1 + dockers/sentinel.conf | 5 + main_test.go | 52 ++++-- monitor_test.go | 10 +- osscluster_test.go | 188 ++++++++++++-------- search_commands.go | 39 ++-- search_test.go | 49 +++++ 16 files changed, 445 insertions(+), 160 deletions(-) create mode 100644 .github/actions/run-tests/action.yml create mode 100644 dockers/.gitignore create mode 100644 dockers/sentinel.conf diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 000000000..95709b5df --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,62 @@ +name: 'Run go-redis tests' +description: 'Runs go-redis tests against different Redis versions and configurations' +inputs: + go-version: + description: 'Go version to use for running tests' + default: '1.23' + redis-version: + description: 'Redis version to test against' + required: true +runs: + using: "composite" + steps: + - name: Set up ${{ inputs.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: Setup Test environment + env: + REDIS_VERSION: ${{ inputs.redis-version }} + CLIENT_LIBS_TEST_IMAGE: "redislabs/client-libs-test:${{ inputs.redis-version }}" + run: | + set -e + redis_major_version=$(echo "$REDIS_VERSION" | grep -oP '^\d+') + if (( redis_major_version < 8 )); then + echo "Using redis-stack for module tests" + else + echo "Using redis CE for module tests" + fi + + # Mapping of redis version to redis testing containers + declare -A redis_version_mapping=( + ["8.0-M03"]="8.0-M04-pre" + ["7.4.2"]="rs-7.4.0-v2" + ["7.2.7"]="rs-7.2.0-v14" + ) + + if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then + echo "REDIS_MAJOR_VERSION=${redis_major_version}" >> $GITHUB_ENV + echo "REDIS_IMAGE=redis:${{ inputs.redis-version }}" >> $GITHUB_ENV + echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV + else + echo "Version not found in the mapping." + exit 1 + fi + sleep 10 # time to settle + shell: bash + - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }} + run: docker compose --profile all up -d + shell: bash + - name: Run tests + env: + RCE_DOCKER: "true" + RE_CLUSTER: "false" + run: | + go test \ + --ginkgo.skip-file="ring_test.go" \ + --ginkgo.skip-file="sentinel_test.go" \ + --ginkgo.skip-file="pubsub_test.go" \ + --ginkgo.skip-file="gears_commands_test.go" \ + --ginkgo.label-filter="!NonRedisEnterprise" + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7578e962e..5852fcde4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,15 +16,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.19.x, 1.20.x, 1.21.x] - - services: - redis: - image: redis/redis-stack-server:latest - options: >- - --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - ports: - - 6379:6379 + go-version: [1.21.x, 1.22.x, 1.23.x] steps: - name: Set up ${{ matrix.go-version }} @@ -50,39 +42,22 @@ jobs: strategy: fail-fast: false matrix: - redis_version: - - "8.0-M01" - - "7.4.1" - - "7.2.6" - - "6.2.16" + redis-version: + - "8.0-M03" # 8.0 milestone 4 + - "7.4.2" # should use redis stack 7.4 + - "7.2.7" # should redis stack 7.2 go-version: - - "1.19.x" - - "1.20.x" - - "1.21.x" + - "1.22.x" + - "1.23.x" steps: - - name: Set up ${{ matrix.go-version }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - - # Set up Docker Compose environment - - name: Set up Docker Compose environment - run: | - docker compose --profile all up -d - + - name: Run tests - env: - USE_CONTAINERIZED_REDIS: "true" - RE_CLUSTER: "true" - run: | - go test \ - --ginkgo.skip-file="ring_test.go" \ - --ginkgo.skip-file="sentinel_test.go" \ - --ginkgo.skip-file="osscluster_test.go" \ - --ginkgo.skip-file="pubsub_test.go" \ - --ginkgo.skip-file="gears_commands_test.go" \ - --ginkgo.label-filter='!NonRedisEnterprise' + uses: ./.github/actions/run-tests + with: + go-version: ${{matrix.go-version}} + redis-version: ${{ matrix.redis-version }} + diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index 6e49e6477..b04f3140b 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.18", "1.19", "1.20", "1.21" ] + go-version: [ "1.21", "1.22", "1.23" ] steps: - name: Set up ${{ matrix.go-version }} diff --git a/.github/workflows/test-redis-enterprise.yml b/.github/workflows/test-redis-enterprise.yml index 1cb36b8d2..10c27198a 100644 --- a/.github/workflows/test-redis-enterprise.yml +++ b/.github/workflows/test-redis-enterprise.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.21.x] + go-version: [1.23.x] re-build: ["7.4.2-54"] steps: @@ -46,8 +46,8 @@ jobs: - name: Test env: - RE_CLUSTER: "1" - USE_CONTAINERIZED_REDIS: "1" + RE_CLUSTER: true + REDIS_MAJOR_VERSION: 7 run: | go test \ --ginkgo.skip-file="ring_test.go" \ diff --git a/.gitignore b/.gitignore index 7507584f0..63b21b0b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -dockers/ *.rdb testdata/* .idea/ diff --git a/Makefile b/Makefile index 1a6bd1786..360505ba5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) +export REDIS_MAJOR_VERSION := 7 test: testdeps + docker start go-redis-redis-stack || docker run -d --name go-redis-redis-stack -p 6379:6379 -e REDIS_ARGS="--enable-debug-command yes --enable-module-command yes" redis/redis-stack-server:latest $(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2)) set -e; for dir in $(GO_MOD_DIRS); do \ if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \ @@ -19,6 +21,7 @@ test: testdeps done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet + docker stop go-redis-redis-stack testdeps: testdata/redis/src/redis-server @@ -32,7 +35,7 @@ build: testdata/redis: mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.4-rc2.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- https://download.redis.io/releases/redis-7.4.2.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis cd $< && make all diff --git a/bench_test.go b/bench_test.go index 8e23303f1..bb84c4156 100644 --- a/bench_test.go +++ b/bench_test.go @@ -277,7 +277,7 @@ func BenchmarkXRead(b *testing.B) { func newClusterScenario() *clusterScenario { return &clusterScenario{ - ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"}, + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, nodeIDs: make([]string, 6), processes: make(map[string]*redisProcess, 6), clients: make(map[string]*redis.Client, 6), diff --git a/commands_test.go b/commands_test.go index 9554bf9a9..901e96e35 100644 --- a/commands_test.go +++ b/commands_test.go @@ -441,7 +441,6 @@ var _ = Describe("Commands", func() { It("should Command", Label("NonRedisEnterprise"), func() { cmds, err := client.Command(ctx).Result() Expect(err).NotTo(HaveOccurred()) - Expect(len(cmds)).To(BeNumerically("~", 240, 25)) cmd := cmds["mget"] Expect(cmd.Name).To(Equal("mget")) diff --git a/docker-compose.yml b/docker-compose.yml index f5ccb8f42..fecd14fef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,140 @@ --- services: - - redis-stanalone: - image: redislabs/client-libs-test:8.0-M02 + redis: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} container_name: redis-standalone environment: + - TLS_ENABLED=yes - REDIS_CLUSTER=no - PORT=6379 - TLS_PORT=6666 - + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} ports: - 6379:6379 - - 6380:6379 - 6666:6666 # TLS port volumes: - - "./dockers/redis-standalone:/redis/work" + - "./dockers/standalone:/redis/work" profiles: - standalone + - sentinel + - all-stack + - all + + cluster: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-cluster + environment: + - NODES=6 + - PORT=16600 + command: "--cluster-enabled yes" + ports: + - "16600-16605:16600-16605" + volumes: + - "./dockers/cluster:/redis/work" + profiles: + - cluster + - all-stack + - all + + sentinel: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26379" + ports: + - 26379:26379 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack - all + + sentinel2: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel2 + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26380" + ports: + - 26380:26380 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack + - all + + sentinel3: + image: ${REDIS_IMAGE:-redis:7.4.1} + container_name: redis-sentinel3 + depends_on: + - redis + entrypoint: "redis-sentinel /redis.conf --port 26381" + ports: + - 26381:26381 + volumes: + - "./dockers/sentinel.conf:/redis.conf" + profiles: + - sentinel + - all-stack + - all + + redisRing1: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-1 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6390 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6390:6390 + volumes: + - "./dockers/ring1:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all + + redisRing2: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-2 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6391 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6391:6391 + volumes: + - "./dockers/ring2:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all + + redisRing3: + image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:7.4.1} + container_name: redis-ring-3 + environment: + - TLS_ENABLED=yes + - REDIS_CLUSTER=no + - PORT=6392 + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} + ports: + - 6392:6392 + volumes: + - "./dockers/ring3:/redis/work" + profiles: + - ring + - cluster + - sentinel + - all-stack + - all \ No newline at end of file diff --git a/dockers/.gitignore b/dockers/.gitignore new file mode 100644 index 000000000..355164c12 --- /dev/null +++ b/dockers/.gitignore @@ -0,0 +1 @@ +*/ diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf new file mode 100644 index 000000000..7d85e430a --- /dev/null +++ b/dockers/sentinel.conf @@ -0,0 +1,5 @@ +sentinel resolve-hostnames yes +sentinel monitor go-redis-test redis 6379 2 +sentinel down-after-milliseconds go-redis-test 5000 +sentinel failover-timeout go-redis-test 60000 +sentinel parallel-syncs go-redis-test 1 diff --git a/main_test.go b/main_test.go index 9f99b5c91..6b3b563a0 100644 --- a/main_test.go +++ b/main_test.go @@ -13,7 +13,6 @@ import ( . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" - "github.com/redis/go-redis/v9" ) @@ -28,7 +27,7 @@ const ( ) const ( - sentinelName = "mymaster" + sentinelName = "go-redis-test" sentinelMasterPort = "9123" sentinelSlave1Port = "9124" sentinelSlave2Port = "9125" @@ -43,8 +42,8 @@ var ( ) var ( - rediStackPort = "6379" - rediStackAddr = ":" + rediStackPort + redisStackPort = "6379" + redisStackAddr = ":" + redisStackPort ) var ( @@ -59,14 +58,22 @@ var ( ) var cluster = &clusterScenario{ - ports: []string{"8220", "8221", "8222", "8223", "8224", "8225"}, + ports: []string{"16600", "16601", "16602", "16603", "16604", "16605"}, nodeIDs: make([]string, 6), processes: make(map[string]*redisProcess, 6), clients: make(map[string]*redis.Client, 6), } +// Redis Software Cluster var RECluster = false -var USE_CONTAINERIZED_REDIS = false + +// Redis Community Edition Docker +var RCEDocker = false + +// Notes the major version of redis we are executing tests. +// This can be used before we change the bsm fork of ginkgo for one, +// which have support for label sets, so we can filter tests per redis major version. +var REDIS_MAJOR_VERSION = 7 func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -83,8 +90,19 @@ var _ = BeforeSuite(func() { } var err error RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) - USE_CONTAINERIZED_REDIS, _ = strconv.ParseBool(os.Getenv("USE_CONTAINERIZED_REDIS")) - if !RECluster || !USE_CONTAINERIZED_REDIS { + RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) + + REDIS_MAJOR_VERSION, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + if REDIS_MAJOR_VERSION == 0 { + REDIS_MAJOR_VERSION = 7 + } + Expect(REDIS_MAJOR_VERSION).To(BeNumerically(">=", 6)) + Expect(REDIS_MAJOR_VERSION).To(BeNumerically("<=", 8)) + + fmt.Printf("RECluster: %v\n", RECluster) + fmt.Printf("RCEDocker: %v\n", RCEDocker) + fmt.Printf("REDIS_MAJOR_VERSION: %v\n", REDIS_MAJOR_VERSION) + if !RECluster && !RCEDocker { redisMain, err = startRedis(redisPort) Expect(err).NotTo(HaveOccurred()) @@ -121,18 +139,24 @@ var _ = BeforeSuite(func() { err = startCluster(ctx, cluster) Expect(err).NotTo(HaveOccurred()) } else { - redisPort = rediStackPort - redisAddr = rediStackAddr + redisPort = redisStackPort + redisAddr = redisStackAddr + + if !RECluster { + // populate cluster node information + Expect(configureClusterTopology(ctx, cluster)).NotTo(HaveOccurred()) + } } }) var _ = AfterSuite(func() { if !RECluster { Expect(cluster.Close()).NotTo(HaveOccurred()) + } - for _, p := range processes { - Expect(p.Close()).NotTo(HaveOccurred()) - } + // NOOP if there are no processes registered + for _, p := range processes { + Expect(p.Close()).NotTo(HaveOccurred()) } processes = nil }) @@ -156,8 +180,8 @@ func redisOptions() *redis.Options { ContextTimeoutEnabled: true, MaxRetries: -1, - PoolSize: 10, + PoolSize: 10, PoolTimeout: 30 * time.Second, ConnMaxIdleTime: time.Minute, } diff --git a/monitor_test.go b/monitor_test.go index 96c33bf1e..ebb784853 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -22,7 +22,7 @@ var _ = Describe("Monitor command", Label("monitor"), func() { if os.Getenv("RUN_MONITOR_TEST") != "true" { Skip("Skipping Monitor command test. Set RUN_MONITOR_TEST=true to run it.") } - client = redis.NewClient(&redis.Options{Addr: ":6379"}) + client = redis.NewClient(&redis.Options{Addr: redisPort}) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) }) @@ -33,7 +33,7 @@ var _ = Describe("Monitor command", Label("monitor"), func() { It("should monitor", Label("monitor"), func() { ress := make(chan string) - client1 := redis.NewClient(&redis.Options{Addr: rediStackAddr}) + client1 := redis.NewClient(&redis.Options{Addr: redisPort}) mn := client1.Monitor(ctx, ress) mn.Start() // Wait for the Redis server to be in monitoring mode. @@ -61,7 +61,7 @@ func TestMonitorCommand(t *testing.T) { } ctx := context.TODO() - client := redis.NewClient(&redis.Options{Addr: ":6379"}) + client := redis.NewClient(&redis.Options{Addr: redisPort}) if err := client.FlushDB(ctx).Err(); err != nil { t.Fatalf("FlushDB failed: %v", err) } @@ -72,8 +72,8 @@ func TestMonitorCommand(t *testing.T) { } }() - ress := make(chan string, 10) // Buffer to prevent blocking - client1 := redis.NewClient(&redis.Options{Addr: ":6379"}) // Adjust the Addr field as necessary + ress := make(chan string, 10) // Buffer to prevent blocking + client1 := redis.NewClient(&redis.Options{Addr: redisPort}) // Adjust the Addr field as necessary mn := client1.Monitor(ctx, ress) mn.Start() // Wait for the Redis server to be in monitoring mode. diff --git a/osscluster_test.go b/osscluster_test.go index 9c3eaba35..93ee464f3 100644 --- a/osscluster_test.go +++ b/osscluster_test.go @@ -25,6 +25,10 @@ type clusterScenario struct { clients map[string]*redis.Client } +func (s *clusterScenario) slots() []int { + return []int{0, 5461, 10923, 16384} +} + func (s *clusterScenario) masters() []*redis.Client { result := make([]*redis.Client, 3) for pos, port := range s.ports[:3] { @@ -83,35 +87,37 @@ func (s *clusterScenario) newClusterClient( } func (s *clusterScenario) Close() error { + ctx := context.TODO() + for _, master := range s.masters() { + err := master.FlushAll(ctx).Err() + if err != nil { + return err + } + + // since 7.2 forget calls should be propagated, calling only master + // nodes should be sufficient. + for _, nID := range s.nodeIDs { + master.ClusterForget(ctx, nID) + } + } + for _, port := range s.ports { if process, ok := processes[port]; ok { - process.Close() + if process != nil { + process.Close() + } + delete(processes, port) } } + return nil } -func startCluster(ctx context.Context, scenario *clusterScenario) error { - // Start processes and collect node ids - for pos, port := range scenario.ports { - process, err := startRedis(port, "--cluster-enabled", "yes") - if err != nil { - return err - } - - client := redis.NewClient(&redis.Options{ - Addr: ":" + port, - }) - - info, err := client.ClusterNodes(ctx).Result() - if err != nil { - return err - } - - scenario.processes[port] = process - scenario.clients[port] = client - scenario.nodeIDs[pos] = info[:40] +func configureClusterTopology(ctx context.Context, scenario *clusterScenario) error { + err := collectNodeInformation(ctx, scenario) + if err != nil { + return err } // Meet cluster nodes. @@ -122,8 +128,7 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { } } - // Bootstrap masters. - slots := []int{0, 5000, 10000, 16384} + slots := scenario.slots() for pos, master := range scenario.masters() { err := master.ClusterAddSlotsRange(ctx, slots[pos], slots[pos+1]-1).Err() if err != nil { @@ -157,35 +162,36 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { // Wait until all nodes have consistent info. wanted := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8220", + Addr: "127.0.0.1:16600", }, { ID: "", - Addr: "127.0.0.1:8223", + Addr: "127.0.0.1:16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8221", + Addr: "127.0.0.1:16601", }, { ID: "", - Addr: "127.0.0.1:8224", + Addr: "127.0.0.1:16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8222", + Addr: "127.0.0.1:16602", }, { ID: "", - Addr: "127.0.0.1:8225", + Addr: "127.0.0.1:16605", }}, }} + for _, client := range scenario.clients { err := eventually(func() error { res, err := client.ClusterSlots(ctx).Result() @@ -193,7 +199,7 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { return err } return assertSlotsEqual(res, wanted) - }, 30*time.Second) + }, 60*time.Second) if err != nil { return err } @@ -202,6 +208,37 @@ func startCluster(ctx context.Context, scenario *clusterScenario) error { return nil } +func collectNodeInformation(ctx context.Context, scenario *clusterScenario) error { + for pos, port := range scenario.ports { + client := redis.NewClient(&redis.Options{ + Addr: ":" + port, + }) + + info, err := client.ClusterNodes(ctx).Result() + if err != nil { + return err + } + + scenario.clients[port] = client + scenario.nodeIDs[pos] = info[:40] + } + return nil +} + +// startCluster start a cluster +func startCluster(ctx context.Context, scenario *clusterScenario) error { + // Start processes and collect node ids + for _, port := range scenario.ports { + process, err := startRedis(port, "--cluster-enabled", "yes") + if err != nil { + return err + } + scenario.processes[port] = process + } + + return configureClusterTopology(ctx, scenario) +} + func assertSlotsEqual(slots, wanted []redis.ClusterSlot) error { outerLoop: for _, s2 := range wanted { @@ -301,17 +338,19 @@ var _ = Describe("ClusterClient", func() { Expect(err).NotTo(HaveOccurred()) } - client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { + err := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { defer GinkgoRecover() Eventually(func() string { return master.Info(ctx, "keyspace").Val() }, 30*time.Second).Should(Or( - ContainSubstring("keys=31"), - ContainSubstring("keys=29"), - ContainSubstring("keys=40"), + ContainSubstring("keys=32"), + ContainSubstring("keys=36"), + ContainSubstring("keys=32"), )) return nil }) + + Expect(err).NotTo(HaveOccurred()) }) It("distributes keys when using EVAL", func() { @@ -327,17 +366,19 @@ var _ = Describe("ClusterClient", func() { Expect(err).NotTo(HaveOccurred()) } - client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { + err := client.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error { defer GinkgoRecover() Eventually(func() string { return master.Info(ctx, "keyspace").Val() }, 30*time.Second).Should(Or( - ContainSubstring("keys=31"), - ContainSubstring("keys=29"), - ContainSubstring("keys=40"), + ContainSubstring("keys=32"), + ContainSubstring("keys=36"), + ContainSubstring("keys=32"), )) return nil }) + + Expect(err).NotTo(HaveOccurred()) }) It("distributes scripts when using Script Load", func() { @@ -347,13 +388,14 @@ var _ = Describe("ClusterClient", func() { script.Load(ctx, client) - client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error { + err := client.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error { defer GinkgoRecover() val, _ := script.Exists(ctx, shard).Result() Expect(val[0]).To(Equal(true)) return nil }) + Expect(err).NotTo(HaveOccurred()) }) It("checks all shards when using Script Exists", func() { @@ -727,33 +769,33 @@ var _ = Describe("ClusterClient", func() { wanted := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8220", + Addr: "127.0.0.1:16600", }, { ID: "", - Addr: "127.0.0.1:8223", + Addr: "127.0.0.1:16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8221", + Addr: "127.0.0.1:16601", }, { ID: "", - Addr: "127.0.0.1:8224", + Addr: "127.0.0.1:16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ ID: "", - Addr: "127.0.0.1:8222", + Addr: "127.0.0.1:16602", }, { ID: "", - Addr: "127.0.0.1:8225", + Addr: "127.0.0.1:16605", }}, }} Expect(assertSlotsEqual(res, wanted)).NotTo(HaveOccurred()) @@ -1122,14 +1164,14 @@ var _ = Describe("ClusterClient", func() { client, err := client.SlaveForKey(ctx, "test") Expect(err).ToNot(HaveOccurred()) info := client.Info(ctx, "server") - Expect(info.Val()).Should(ContainSubstring("tcp_port:8224")) + Expect(info.Val()).Should(ContainSubstring("tcp_port:16604")) }) It("should return correct master for key", func() { client, err := client.MasterForKey(ctx, "test") Expect(err).ToNot(HaveOccurred()) info := client.Info(ctx, "server") - Expect(info.Val()).Should(ContainSubstring("tcp_port:8221")) + Expect(info.Val()).Should(ContainSubstring("tcp_port:16601")) }) assertClusterClient() @@ -1176,18 +1218,18 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard1Port, }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard2Port, }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard3Port, @@ -1230,18 +1272,18 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard1Port, }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard2Port, }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ Addr: ":" + ringShard3Port, @@ -1284,27 +1326,27 @@ var _ = Describe("ClusterClient", func() { opt.ClusterSlots = func(ctx context.Context) ([]redis.ClusterSlot, error) { slots := []redis.ClusterSlot{{ Start: 0, - End: 4999, + End: 5460, Nodes: []redis.ClusterNode{{ - Addr: ":8220", + Addr: ":16600", }, { - Addr: ":8223", + Addr: ":16603", }}, }, { - Start: 5000, - End: 9999, + Start: 5461, + End: 10922, Nodes: []redis.ClusterNode{{ - Addr: ":8221", + Addr: ":16601", }, { - Addr: ":8224", + Addr: ":16604", }}, }, { - Start: 10000, + Start: 10923, End: 16383, Nodes: []redis.ClusterNode{{ - Addr: ":8222", + Addr: ":16602", }, { - Addr: ":8225", + Addr: ":16605", }}, }} return slots, nil diff --git a/search_commands.go b/search_commands.go index ede084e4e..9e5928017 100644 --- a/search_commands.go +++ b/search_commands.go @@ -282,23 +282,30 @@ type FTSearchSortBy struct { Desc bool } +// FTSearchOptions hold options that can be passed to the FT.SEARCH command. +// More information about the options can be found +// in the documentation for FT.SEARCH https://redis.io/docs/latest/commands/ft.search/ type FTSearchOptions struct { - NoContent bool - Verbatim bool - NoStopWords bool - WithScores bool - WithPayloads bool - WithSortKeys bool - Filters []FTSearchFilter - GeoFilter []FTSearchGeoFilter - InKeys []interface{} - InFields []interface{} - Return []FTSearchReturn - Slop int - Timeout int - InOrder bool - Language string - Expander string + NoContent bool + Verbatim bool + NoStopWords bool + WithScores bool + WithPayloads bool + WithSortKeys bool + Filters []FTSearchFilter + GeoFilter []FTSearchGeoFilter + InKeys []interface{} + InFields []interface{} + Return []FTSearchReturn + Slop int + Timeout int + InOrder bool + Language string + Expander string + // Scorer is used to set scoring function, if not set passed, a default will be used. + // The default scorer depends on the Redis version: + // - `BM25` for Redis >= 8 + // - `TFIDF` for Redis < 8 Scorer string ExplainScore bool Payload string diff --git a/search_test.go b/search_test.go index e267c8ae8..a48f45bf0 100644 --- a/search_test.go +++ b/search_test.go @@ -371,7 +371,56 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(names).To(ContainElement("John")) }) + // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 + // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + if REDIS_MAJOR_VERSION < 8 { + Skip("(redis major version < 8) default scorer is not BM25") + } + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "doc1", "description", "The quick brown fox jumps over the lazy dog") + client.HSet(ctx, "doc2", "description", "Quick alice was beginning to get very tired of sitting by her quick sister on the bank, and of having nothing to do.") + + res, err := client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.236)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "TFIDF.DOCNORM"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(0.14285714285714285)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "BM25"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeNumerically("<=", 0.22471909420069797)) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DISMAX"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(2))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "DOCSCORE"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(1))) + + res, err = client.FTSearchWithArgs(ctx, "idx1", "quick", &redis.FTSearchOptions{WithScores: true, Scorer: "HAMMING"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(*res.Docs[0].Score).To(BeEquivalentTo(float64(0))) + }) + + // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 + // this test expect redis major version <=7 + It("should FTSearch WithScores", Label("search", "ftsearch"), func() { + if REDIS_MAJOR_VERSION > 7 { + Skip("(redis major version > 7) default scorer is not TFIDF") + } text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) From d0fb810b13dba5c653a9fab136c497c6349a1159 Mon Sep 17 00:00:00 2001 From: Shawn Wang <62313353+shawnwgit@users.noreply.github.com> Date: Mon, 3 Feb 2025 06:15:00 -0800 Subject: [PATCH 40/68] Fix race condition in clusterNodes.Addrs() (#3219) Resolve a race condition in the clusterNodes.Addrs() method. Previously, the method returned a reference to a string slice, creating the potential for concurrent reads by the caller while the slice was being modified by the garbage collection process. Co-authored-by: Nedyalko Dyakov --- osscluster.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osscluster.go b/osscluster.go index 72e922a80..188f50359 100644 --- a/osscluster.go +++ b/osscluster.go @@ -487,9 +487,11 @@ func (c *clusterNodes) Addrs() ([]string, error) { closed := c.closed //nolint:ifshort if !closed { if len(c.activeAddrs) > 0 { - addrs = c.activeAddrs + addrs = make([]string, len(c.activeAddrs)) + copy(addrs, c.activeAddrs) } else { - addrs = c.addrs + addrs = make([]string, len(c.addrs)) + copy(addrs, c.addrs) } } c.mu.RUnlock() From a39be3727325dae492684209207d4822fd0b87df Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 3 Feb 2025 19:10:54 +0200 Subject: [PATCH 41/68] feat(tests): validate that ConfigSet and ConfigGet work with Modules (#3258) * Add tests for unified config in Redis 8 * WIP: fix reading FT.CONFIG with RESP3 * add more tests * use search-timeout * move deprecated warnings on the bottom --- .gitignore | 1 + command.go | 48 +++++++++++----- commands_test.go | 138 +++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 30 +++++++--- search_commands.go | 24 ++++++-- search_test.go | 67 ++++++++++++++++++++-- 6 files changed, 274 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 63b21b0b4..f1883206a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ testdata/* .DS_Store *.tar.gz *.dic +redis8tests.sh diff --git a/command.go b/command.go index f5aad9149..2623a2396 100644 --- a/command.go +++ b/command.go @@ -3862,30 +3862,48 @@ func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { return cmd.val } +// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3 func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadArrayLen() + data, err := rd.ReadReply() if err != nil { return err } + resultMap := map[string]interface{}{} - data := make(map[string]interface{}, n/2) - for i := 0; i < n; i += 2 { - _, err := rd.ReadArrayLen() - if err != nil { - cmd.err = err - } - key, err := rd.ReadString() - if err != nil { - cmd.err = err + switch midResponse := data.(type) { + case map[interface{}]interface{}: // resp3 will return map + for k, v := range midResponse { + stringKey, ok := k.(string) + if !ok { + return fmt.Errorf("redis: invalid map key %#v", k) + } + resultMap[stringKey] = v } - value, err := rd.ReadString() - if err != nil { - cmd.err = err + case []interface{}: // resp2 will return array of arrays + n := len(midResponse) + for i := 0; i < n; i++ { + finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map + if !ok { + return fmt.Errorf("redis: unexpected response %#v", data) + } + m := len(finalArr) + if m%2 != 0 { // since this should be map, keys should be even number + return fmt.Errorf("redis: unexpected response %#v", data) + } + + for j := 0; j < m; j += 2 { + stringKey, ok := finalArr[j].(string) // the first one + if !ok { + return fmt.Errorf("redis: invalid map key %#v", finalArr[i]) + } + resultMap[stringKey] = finalArr[j+1] // second one is value + } } - data[key] = value + default: + return fmt.Errorf("redis: unexpected response %#v", data) } - cmd.val = data + cmd.val = resultMap return nil } diff --git a/commands_test.go b/commands_test.go index 901e96e35..dacc7f3d5 100644 --- a/commands_test.go +++ b/commands_test.go @@ -344,6 +344,23 @@ var _ = Describe("Commands", func() { Expect(val).NotTo(BeEmpty()) }) + It("should ConfigGet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-*": "search-timeout", + "ts-*": "ts-retention-policy", + "bf-*": "bf-error-rate", + "cf-*": "cf-initial-size", + } + + for prefix, lookup := range expected { + val, err := client.ConfigGet(ctx, prefix).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[lookup]).NotTo(BeEmpty()) + } + }) + It("should ConfigResetStat", Label("NonRedisEnterprise"), func() { r := client.ConfigResetStat(ctx) Expect(r.Err()).NotTo(HaveOccurred()) @@ -362,6 +379,127 @@ var _ = Describe("Commands", func() { Expect(configSet.Val()).To(Equal("OK")) }) + It("should ConfigGet with Modules", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisMajor(8, "config get won't return modules configs before redis 8") + configGet := client.ConfigGet(ctx, "*") + Expect(configGet.Err()).NotTo(HaveOccurred()) + Expect(configGet.Val()).To(HaveKey("maxmemory")) + Expect(configGet.Val()).To(HaveKey("search-timeout")) + Expect(configGet.Val()).To(HaveKey("ts-retention-policy")) + Expect(configGet.Val()).To(HaveKey("bf-error-rate")) + Expect(configGet.Val()).To(HaveKey("cf-initial-size")) + }) + + It("should ConfigSet FT DIALECT", func() { + SkipBeforeRedisMajor(8, "config doesn't include modules before Redis 8") + defaultState, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + + // set to 3 + res, err := client.ConfigSet(ctx, "search-default-dialect", "3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "3"})) + + resGet, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "3"})) + + // set to 2 + res, err = client.ConfigSet(ctx, "search-default-dialect", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + + // set to 1 + res, err = client.ConfigSet(ctx, "search-default-dialect", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + resGet, err = client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "1"})) + + // set to default + res, err = client.ConfigSet(ctx, "search-default-dialect", defaultState["search-default-dialect"]).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + }) + + It("should ConfigSet fail for ReadOnly", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + _, err := client.ConfigSet(ctx, "search-max-doctablesize", "100000").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + defaults := map[string]string{} + expected := map[string]string{ + "search-timeout": "100", + "ts-retention-policy": "2", + "bf-error-rate": "0.13", + "cf-initial-size": "64", + } + + // read the defaults to set them back later + for setting, _ := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + defaults[setting] = val[setting] + } + + // check if new values can be set + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + + for setting, value := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[setting]).To(Equal(value)) + } + + // set back to the defaults + for setting, value := range defaults { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + }) + + It("should Fail ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-timeout": "-100", + "ts-retention-policy": "-10", + "bf-error-rate": "1.5", + "cf-initial-size": "-10", + } + + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(setting))) + Expect(val).To(BeEmpty()) + } + }) + It("should ConfigRewrite", Label("NonRedisEnterprise"), func() { configRewrite := client.ConfigRewrite(ctx) Expect(configRewrite.Err()).NotTo(HaveOccurred()) diff --git a/main_test.go b/main_test.go index 6b3b563a0..a326960a0 100644 --- a/main_test.go +++ b/main_test.go @@ -73,7 +73,19 @@ var RCEDocker = false // Notes the major version of redis we are executing tests. // This can be used before we change the bsm fork of ginkgo for one, // which have support for label sets, so we can filter tests per redis major version. -var REDIS_MAJOR_VERSION = 7 +var RedisMajorVersion = 7 + +func SkipBeforeRedisMajor(version int, msg string) { + if RedisMajorVersion < version { + Skip(fmt.Sprintf("(redis major version < %d) %s", version, msg)) + } +} + +func SkipAfterRedisMajor(version int, msg string) { + if RedisMajorVersion > version { + Skip(fmt.Sprintf("(redis major version > %d) %s", version, msg)) + } +} func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -92,16 +104,20 @@ var _ = BeforeSuite(func() { RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) - REDIS_MAJOR_VERSION, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) - if REDIS_MAJOR_VERSION == 0 { - REDIS_MAJOR_VERSION = 7 + RedisMajorVersion, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + + if RedisMajorVersion == 0 { + RedisMajorVersion = 7 } - Expect(REDIS_MAJOR_VERSION).To(BeNumerically(">=", 6)) - Expect(REDIS_MAJOR_VERSION).To(BeNumerically("<=", 8)) fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_MAJOR_VERSION: %v\n", REDIS_MAJOR_VERSION) + fmt.Printf("REDIS_MAJOR_VERSION: %v\n", RedisMajorVersion) + + if RedisMajorVersion < 6 || RedisMajorVersion > 8 { + panic("incorrect or not supported redis major version") + } + if !RECluster && !RCEDocker { redisMain, err = startRedis(redisPort) diff --git a/search_commands.go b/search_commands.go index 9e5928017..1312a78f0 100644 --- a/search_commands.go +++ b/search_commands.go @@ -831,20 +831,32 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool return cmd } -// FTConfigGet - Retrieves the value of a RediSearch configuration parameter. +// Retrieves the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to retrieve. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/) +// For more information, please refer to the Redis [FT.CONFIG GET] documentation. +// +// Deprecated: FTConfigGet is deprecated in Redis 8. +// All configuration will be done with the CONFIG GET command. +// For more information check [Client.ConfigGet] and [CONFIG GET Documentation] +// +// [CONFIG GET Documentation]: https://redis.io/commands/config-get/ +// [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/ func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd { cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) _ = c(ctx, cmd) return cmd } -// FTConfigSet - Sets the value of a RediSearch configuration parameter. +// Sets the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG SET]: (https://redis.io/commands/ft.config-set/) +// For more information, please refer to the Redis [FT.CONFIG SET] documentation. +// +// Deprecated: FTConfigSet is deprecated in Redis 8. +// All configuration will be done with the CONFIG SET command. +// For more information check [Client.ConfigSet] and [CONFIG SET Documentation] +// +// [CONFIG SET Documentation]: https://redis.io/commands/config-set/ +// [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/ func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) _ = c(ctx, cmd) diff --git a/search_test.go b/search_test.go index a48f45bf0..0a06ffef8 100644 --- a/search_test.go +++ b/search_test.go @@ -374,9 +374,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION < 8 { - Skip("(redis major version < 8) default scorer is not BM25") - } + SkipBeforeRedisMajor(8, "default scorer is not BM25") + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -418,9 +417,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version <=7 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION > 7 { - Skip("(redis major version > 7) default scorer is not TFIDF") - } + SkipAfterRedisMajor(7, "default scorer is not TFIDF") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -1015,6 +1012,24 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) + It("should FTConfigGet return multiple fields", Label("search", "NonRedisEnterprise"), func() { + res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + }) + It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() Expect(err).NotTo(HaveOccurred()) @@ -1471,6 +1486,46 @@ func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []str // Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) // }) +var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "NonRedisEnterprise"), func() { + + var clientResp2 *redis.Client + var clientResp3 *redis.Client + BeforeEach(func() { + clientResp2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + clientResp3 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true}) + Expect(clientResp3.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(clientResp2.Close()).NotTo(HaveOccurred()) + Expect(clientResp3.Close()).NotTo(HaveOccurred()) + }) + + It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + val, err := clientResp3.FTConfigSet(ctx, "TIMEOUT", "100").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res2, err := clientResp2.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + + res3, err := clientResp3.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + }) + + It("should FTConfigGet all resp2 and resp3", Label("search", "NonRedisEnterprise"), func() { + res2, err := clientResp2.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + + res3, err := clientResp3.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res3)).To(BeEquivalentTo(len(res2))) + Expect(res2["DEFAULT_DIALECT"]).To(BeEquivalentTo(res2["DEFAULT_DIALECT"])) + }) +}) + var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { ctx := context.TODO() var client *redis.Client From beb8692fc3fb5b1e95700cf589521451adf1c4c1 Mon Sep 17 00:00:00 2001 From: ZhuHaiCheng Date: Tue, 4 Feb 2025 01:29:02 +0800 Subject: [PATCH 42/68] chore: fix some comments (#3226) Signed-off-by: zhuhaicity Co-authored-by: Nedyalko Dyakov --- hash_commands.go | 2 +- search_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hash_commands.go b/hash_commands.go index dcffdcdd9..6596c6f5f 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -225,7 +225,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati return cmd } -// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds. // It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. // For more information - https://redis.io/commands/hexpire/ diff --git a/search_test.go b/search_test.go index 0a06ffef8..a409fc78a 100644 --- a/search_test.go +++ b/search_test.go @@ -136,7 +136,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "txt") - client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis") + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result() Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(BeEquivalentTo(int64(1))) @@ -482,7 +482,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { WaitForIndexing(client, "idx1") client.HSet(ctx, "search", "title", "RediSearch", - "body", "Redisearch impements a search engine on top of redis", + "body", "Redisearch implements a search engine on top of redis", "parent", "redis", "random_num", 10) client.HSet(ctx, "ai", "title", "RedisAI", From d8a9655c2160765eec2849f124323b4c47ab717c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:28:08 +0200 Subject: [PATCH 43/68] chore(deps): bump github.com/cespare/xxhash/v2 from 2.2.0 to 2.3.0 (#2964) Bumps [github.com/cespare/xxhash/v2](https://github.com/cespare/xxhash) from 2.2.0 to 2.3.0. - [Commits](https://github.com/cespare/xxhash/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: github.com/cespare/xxhash/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c1d9037ac..1492d2709 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/cespare/xxhash/v2 v2.2.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ) diff --git a/go.sum b/go.sum index 21b4f64ee..4db68f6d4 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= From 3d4310ae9696cadc8eaeab747158abe1d03d8b7a Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Tue, 4 Feb 2025 10:34:08 +0100 Subject: [PATCH 44/68] feat(options): add skip_verify param (#3216) * feat(options): Add skip_verify param When parsing a URL, add a "skip_verify" query param to disable TLS certificate verification. Inspired by various Go drivers: * ClickHouse: https://github.com/ClickHouse/clickhouse-go/blob/v2.30.0/clickhouse_options.go#L259 * MongoDB: https://github.com/mongodb/mongo-go-driver/blob/v2.0.0/x/mongo/driver/connstring/connstring.go#L609 * MySQL: https://github.com/go-sql-driver/mysql/blob/v1.8.1/dsn.go#L175 Signed-off-by: Julien Riou * docs(options): Add skip_verify to ParseURL Signed-off-by: Julien Riou --------- Signed-off-by: Julien Riou Co-authored-by: Nedyalko Dyakov --- options.go | 4 ++++ options_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/options.go b/options.go index 8ba74ccd1..b9701702f 100644 --- a/options.go +++ b/options.go @@ -267,6 +267,7 @@ func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, er // URL attributes (scheme, host, userinfo, resp.), query parameters using these // names will be treated as unknown parameters // - unknown parameter names will result in an error +// - use "skip_verify=true" to ignore TLS certificate validation // // Examples: // @@ -487,6 +488,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) { if q.err != nil { return nil, q.err } + if o.TLSConfig != nil && q.has("skip_verify") { + o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify") + } // any parameters left? if r := q.remaining(); len(r) > 0 { diff --git a/options_test.go b/options_test.go index 1db36fdb4..d46ecc858 100644 --- a/options_test.go +++ b/options_test.go @@ -30,6 +30,9 @@ func TestParseURL(t *testing.T) { }, { url: "rediss://localhost:123", o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{ /* no deep comparison */ }}, + }, { + url: "rediss://localhost:123/?skip_verify=true", + o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{InsecureSkipVerify: true}}, }, { url: "redis://:bar@localhost:123", o: &Options{Addr: "localhost:123", Password: "bar"}, From 0b34b1909ae443b9447451e263b39b7e428651be Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Wed, 5 Feb 2025 15:44:09 +0200 Subject: [PATCH 45/68] feat(command): add ACL commands, validate module categories exist (#3262) * add ACL{SetUser,DelUser,List} commands * test presence of categories in acl cat * code cleanup * add basic acl tests * add acl modules tests * reset acl log before test * refactor acl tests * fix clientkillbyage test --- acl_commands.go | 54 ++++++ acl_commands_test.go | 449 +++++++++++++++++++++++++++++++++++++++++++ commands_test.go | 55 +----- 3 files changed, 505 insertions(+), 53 deletions(-) create mode 100644 acl_commands_test.go diff --git a/acl_commands.go b/acl_commands.go index 06847be2e..9cb800bb3 100644 --- a/acl_commands.go +++ b/acl_commands.go @@ -4,8 +4,20 @@ import "context" type ACLCmdable interface { ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd + ACLLog(ctx context.Context, count int64) *ACLLogCmd ACLLogReset(ctx context.Context) *StatusCmd + + ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd + ACLDelUser(ctx context.Context, username string) *IntCmd + ACLList(ctx context.Context) *StringSliceCmd + + ACLCat(ctx context.Context) *StringSliceCmd + ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd +} + +type ACLCatArgs struct { + Category string } func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { @@ -33,3 +45,45 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { _ = c(ctx, cmd) return cmd } + +func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd { + cmd := NewIntCmd(ctx, "acl", "deluser", username) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd { + args := make([]interface{}, 3+len(rules)) + args[0] = "acl" + args[1] = "setuser" + args[2] = username + for i, rule := range rules { + args[i+3] = rule + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "list") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "cat") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd { + // if there is a category passed, build new cmd, if there isn't - use the ACLCat method + if options != nil && options.Category != "" { + cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category) + _ = c(ctx, cmd) + return cmd + } + + return c.ACLCat(ctx) +} diff --git a/acl_commands_test.go b/acl_commands_test.go new file mode 100644 index 000000000..846455831 --- /dev/null +++ b/acl_commands_test.go @@ -0,0 +1,449 @@ +package redis_test + +import ( + "context" + + "github.com/redis/go-redis/v9" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" +) + +var TestUserName string = "goredis" +var _ = Describe("ACL", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + It("should ACL LOG", Label("NonRedisEnterprise"), func() { + Expect(client.ACLLogReset(ctx).Err()).NotTo(HaveOccurred()) + err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() + Expect(err).NotTo(HaveOccurred()) + + clientAcl := redis.NewClient(redisOptions()) + clientAcl.Options().Username = "test" + clientAcl.Options().Password = "test" + clientAcl.Options().DB = 0 + _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() + _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() + _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() + + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(4)) + + for _, entry := range logEntries { + Expect(entry.Reason).To(Equal("command")) + Expect(entry.Context).To(Equal("toplevel")) + Expect(entry.Object).NotTo(BeEmpty()) + Expect(entry.Username).To(Equal("test")) + Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) + Expect(entry.ClientInfo).NotTo(BeNil()) + Expect(entry.EntryID).To(BeNumerically(">=", 0)) + Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) + Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) + } + + limitedLogEntries, err := client.ACLLog(ctx, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(limitedLogEntries)).To(Equal(2)) + + // cleanup after creating the user + err = client.Do(ctx, "acl", "deluser", "test").Err() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { + // Call ACL LOG RESET + resetCmd := client.ACLLogReset(ctx) + Expect(resetCmd.Err()).NotTo(HaveOccurred()) + Expect(resetCmd.Val()).To(Equal("OK")) + + // Verify that the log is empty after the reset + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(0)) + }) + +}) +var _ = Describe("ACL user commands", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("list only default user", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + }) + + It("setuser and deluser", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + + add, err := client.ACLSetUser(ctx, TestUserName, "nopass", "on", "allkeys", "+set", "+get").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + resAfter, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfter).To(HaveLen(2)) + Expect(resAfter[1]).To(ContainSubstring(TestUserName)) + + deletedN, err := client.ACLDelUser(ctx, TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(deletedN).To(BeNumerically("==", 1)) + + resAfterDeletion, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfterDeletion).To(HaveLen(1)) + Expect(resAfterDeletion[0]).To(BeEquivalentTo(res[0])) + }) + + It("should acl dryrun", func() { + dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") + Expect(dryRun.Err()).NotTo(HaveOccurred()) + Expect(dryRun.Val()).To(Equal("OK")) + }) +}) + +var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + opt.UnstableResp3 = true + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("reset permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + _, err = connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + }) + + It("add write permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+SET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can write + v, err := connection.Set(ctx, "anykey", "anyvalue", 0).Result() + Expect(err).ToNot(HaveOccurred()) + Expect(v).To(Equal("OK")) + + // but can't read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(value).To(BeEmpty()) + }) + + It("add read permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+GET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(Equal("anyvalue")) + + // but can't delete + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(del).ToNot(Equal(1)) + }) + + It("add del permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+DEL", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(del).To(BeEquivalentTo(1)) + }) + + It("set permissions for module commands", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+FT.SEARCH", + "-FT.DROPINDEX", + "+json.set", + "+json.get", + "-json.clear", + "+bf.reserve", + "-bf.info", + "+cf.reserve", + "+cms.initbydim", + "+topk.reserve", + "+tdigest.create", + "+ts.create", + "-ts.info", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + + // no perm for dropindex + err = c.FTDropIndex(ctx, "txt").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + + // no perm for json clear + err = c.JSONClear(ctx, "foo", "$").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + + // no perm for info + err = c.BFInfo(ctx, "bloom").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // noperm for ts.info + err = c.TSInfo(ctx, "tsts").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + It("set permissions for module categories", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+@search", + "+@json", + "+@bloom", + "+@cuckoo", + "+@topk", + "+@cms", + "+@timeseries", + "+@tdigest", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + // perm for dropindex + Expect(c.FTDropIndex(ctx, "txt").Err()).NotTo(HaveOccurred()) + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + // perm for json clear + Expect(c.JSONClear(ctx, "foo", "$").Err()).NotTo(HaveOccurred()) + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + // perm for info + Expect(c.BFInfo(ctx, "bloom").Err()).NotTo(HaveOccurred()) + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // perm for ts.info + Expect(c.TSInfo(ctx, "tsts").Err()).NotTo(HaveOccurred()) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("ACL Categories", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("lists acl categories and subcategories", func() { + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res)).To(BeNumerically(">", 20)) + Expect(res).To(ContainElements( + "read", + "write", + "keyspace", + "dangerous", + "slow", + "set", + "sortedset", + "list", + "hash", + )) + + res, err = client.ACLCatArgs(ctx, &redis.ACLCatArgs{Category: "read"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement("get")) + }) + + It("lists acl categories and subcategories with Modules", func() { + SkipBeforeRedisMajor(8, "modules are included in acl for redis version >= 8") + aclTestCase := map[string]string{ + "search": "FT.CREATE", + "bloom": "bf.add", + "json": "json.get", + "cuckoo": "cf.insert", + "cms": "cms.query", + "topk": "topk.list", + "tdigest": "tdigest.rank", + "timeseries": "ts.range", + } + var cats []interface{} + + for cat, subitem := range aclTestCase { + cats = append(cats, cat) + + res, err := client.ACLCatArgs(ctx, &redis.ACLCatArgs{ + Category: cat, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement(subitem)) + } + + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElements(cats...)) + }) +}) diff --git a/commands_test.go b/commands_test.go index dacc7f3d5..404ffd02b 100644 --- a/commands_test.go +++ b/commands_test.go @@ -211,13 +211,13 @@ var _ = Describe("Commands", func() { select { case <-done: Fail("BLPOP is not blocked.") - case <-time.After(2 * time.Second): + case <-time.After(1 * time.Second): // ok } killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)))) + Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)), Equal(int64(4)))) select { case <-done: @@ -2228,12 +2228,6 @@ var _ = Describe("Commands", func() { Expect(replace.Val()).To(Equal(int64(1))) }) - It("should acl dryrun", func() { - dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") - Expect(dryRun.Err()).NotTo(HaveOccurred()) - Expect(dryRun.Val()).To(Equal("OK")) - }) - It("should fail module loadex", Label("NonRedisEnterprise"), func() { dryRun := client.ModuleLoadex(ctx, &redis.ModuleLoadexConfig{ Path: "/path/to/non-existent-library.so", @@ -2281,51 +2275,6 @@ var _ = Describe("Commands", func() { Expect(args).To(Equal(expectedArgs)) }) - - It("should ACL LOG", Label("NonRedisEnterprise"), func() { - err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() - Expect(err).NotTo(HaveOccurred()) - - clientAcl := redis.NewClient(redisOptions()) - clientAcl.Options().Username = "test" - clientAcl.Options().Password = "test" - clientAcl.Options().DB = 0 - _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() - _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() - _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() - - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(4)) - - for _, entry := range logEntries { - Expect(entry.Reason).To(Equal("command")) - Expect(entry.Context).To(Equal("toplevel")) - Expect(entry.Object).NotTo(BeEmpty()) - Expect(entry.Username).To(Equal("test")) - Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) - Expect(entry.ClientInfo).NotTo(BeNil()) - Expect(entry.EntryID).To(BeNumerically(">=", 0)) - Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) - Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) - } - - limitedLogEntries, err := client.ACLLog(ctx, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(limitedLogEntries)).To(Equal(2)) - }) - - It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { - // Call ACL LOG RESET - resetCmd := client.ACLLogReset(ctx) - Expect(resetCmd.Err()).NotTo(HaveOccurred()) - Expect(resetCmd.Val()).To(Equal("OK")) - - // Verify that the log is empty after the reset - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(0)) - }) }) Describe("hashes", func() { From 3cef28060203d9ae668baa499e21ac6e55619b83 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:31:01 +0000 Subject: [PATCH 46/68] DOC-4734 added geo indexing examples (#3240) * DOC-4734 added geo indexing examples * DOC-4734 delete keys before starting tests --------- Co-authored-by: Nedyalko Dyakov --- doctests/geo_index_test.go | 206 +++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 doctests/geo_index_test.go diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go new file mode 100644 index 000000000..9c38ba9d3 --- /dev/null +++ b/doctests/geo_index_test.go @@ -0,0 +1,206 @@ +// EXAMPLE: geoindex +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoindex() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // REMOVE_START + rdb.FTDropIndex(ctx, "productidx") + rdb.FTDropIndex(ctx, "geomidx") + rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") + // REMOVE_END + + // STEP_START create_geo_idx + geoCreateResult, err := rdb.FTCreate(ctx, + "productidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"product:"}, + }, + &redis.FieldSchema{ + FieldName: "$.location", + As: "location", + FieldType: redis.SearchFieldTypeGeo, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoCreateResult) // >>> OK + // STEP_END + + // STEP_START add_geo_json + prd46885 := map[string]interface{}{ + "description": "Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "-104.991531, 39.742043", + } + + gjResult1, err := rdb.JSONSet(ctx, "product:46885", "$", prd46885).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult1) // >>> OK + + prd46886 := map[string]interface{}{ + "description": "Bright Green Socks", + "price": 25.50, + "city": "Fort Collins", + "location": "-105.0618814,40.5150098", + } + + gjResult2, err := rdb.JSONSet(ctx, "product:46886", "$", prd46886).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult2) // >>> OK + // STEP_END + + // STEP_START geo_query + geoQueryResult, err := rdb.FTSearch(ctx, "productidx", + "@location:[-104.800644 38.846127 100 mi]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoQueryResult) + // >>> {1 [{product:46885... + // STEP_END + + // STEP_START create_gshape_idx + geomCreateResult, err := rdb.FTCreate(ctx, "geomidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"shape:"}, + }, + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.geom", + As: "geom", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomCreateResult) // >>> OK + // STEP_END + + // STEP_START add_gshape_json + shape1 := map[string]interface{}{ + "name": "Green Square", + "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + } + + gmjResult1, err := rdb.JSONSet(ctx, "shape:1", "$", shape1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult1) // >>> OK + + shape2 := map[string]interface{}{ + "name": "Red Rectangle", + "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))", + } + + gmjResult2, err := rdb.JSONSet(ctx, "shape:2", "$", shape2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult2) // >>> OK + + shape3 := map[string]interface{}{ + "name": "Blue Triangle", + "geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))", + } + + gmjResult3, err := rdb.JSONSet(ctx, "shape:3", "$", shape3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult3) // >>> OK + + shape4 := map[string]interface{}{ + "name": "Purple Point", + "geom": "POINT (2 2)", + } + + gmjResult4, err := rdb.JSONSet(ctx, "shape:4", "$", shape4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult4) // >>> OK + // STEP_END + + // STEP_START gshape_query + geomQueryResult, err := rdb.FTSearchWithArgs(ctx, "geomidx", + "(-@name:(Green Square) @geom:[WITHIN $qshape])", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + }, + DialectVersion: 4, + Limit: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomQueryResult) + // >>> {1 [{shape:4... + // STEP_END + + // Output: + // OK + // OK + // OK + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // OK + // OK + // OK + // OK + // OK + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} +} From 86bb1433cae74b230da6e9c9fe7399f01eeeb7cf Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:33:11 +0000 Subject: [PATCH 47/68] DOC-4799 fixed capped list example (#3260) Co-authored-by: Nedyalko Dyakov --- doctests/list_tutorial_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go index 908469ce0..bec1e1643 100644 --- a/doctests/list_tutorial_test.go +++ b/doctests/list_tutorial_test.go @@ -388,7 +388,7 @@ func ExampleClient_ltrim() { // REMOVE_END // STEP_START ltrim - res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + res27, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() if err != nil { panic(err) @@ -410,13 +410,13 @@ func ExampleClient_ltrim() { panic(err) } - fmt.Println(res29) // >>> [bike:5 bike:4 bike:3] + fmt.Println(res29) // >>> [bike:1 bike:2 bike:3] // STEP_END // Output: // 5 // OK - // [bike:5 bike:4 bike:3] + // [bike:1 bike:2 bike:3] } func ExampleClient_ltrim_end_of_list() { From fb809bb642a1db7fd967daeb8bcd5217680379e0 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:34:51 +0000 Subject: [PATCH 48/68] DOC-4331 added full text query examples (#3256) * DOC-4331 added full text query examples * DOC-4331 made tests deterministic with sort --------- Co-authored-by: Nedyalko Dyakov --- doctests/query_ft_test.go | 331 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 doctests/query_ft_test.go diff --git a/doctests/query_ft_test.go b/doctests/query_ft_test.go new file mode 100644 index 000000000..095230f73 --- /dev/null +++ b/doctests/query_ft_test.go @@ -0,0 +1,331 @@ +// EXAMPLE: query_ft +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_ft() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START ft1 + res1, err := rdb.FTSearch(ctx, + "idx:bicycle", "@description: kids", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 2 + + sort.Slice(res1.Docs, func(i, j int) bool { + return res1.Docs[i].ID < res1.Docs[j].ID + }) + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:1 + // >>> bicycle:2 + // STEP_END + + // STEP_START ft2 + res2, err := rdb.FTSearch(ctx, + "idx:bicycle", "@model: ka*", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:4 + // STEP_END + + // STEP_START ft3 + res3, err := rdb.FTSearch(ctx, + "idx:bicycle", "@brand: *bikes", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 2 + + sort.Slice(res3.Docs, func(i, j int) bool { + return res3.Docs[i].ID < res3.Docs[j].ID + }) + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:4 + // >>> bicycle:6 + // STEP_END + + // STEP_START ft4 + res4, err := rdb.FTSearch(ctx, + "idx:bicycle", "%optamized%", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4.Total) // >>> 1 + + for _, doc := range res4.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:3 + // STEP_END + + // STEP_START ft5 + res5, err := rdb.FTSearch(ctx, + "idx:bicycle", "%%optamised%%", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5.Total) // >>> 1 + + for _, doc := range res5.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:3 + // STEP_END + + // Output: + // 2 + // bicycle:1 + // bicycle:2 + // 1 + // bicycle:4 + // 2 + // bicycle:4 + // bicycle:6 + // 1 + // bicycle:3 + // 1 + // bicycle:3 +} From 2418111e62557903ae12f0b941aad80ae8d7b5bd Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:42:38 +0000 Subject: [PATCH 49/68] DOC-4332 added geo query examples (#3257) Co-authored-by: Nedyalko Dyakov --- doctests/query_geo_test.go | 327 +++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 doctests/query_geo_test.go diff --git a/doctests/query_geo_test.go b/doctests/query_geo_test.go new file mode 100644 index 000000000..7e880aead --- /dev/null +++ b/doctests/query_geo_test.go @@ -0,0 +1,327 @@ +// EXAMPLE: query_geo +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_geo() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + &redis.FieldSchema{ + FieldName: "$.store_location", + As: "store_location", + FieldType: redis.SearchFieldTypeGeo, + }, + &redis.FieldSchema{ + FieldName: "$.pickup_zone", + As: "pickup_zone", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START geo1 + res1, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "@store_location:[$lon $lat $radius $units]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "lon": -0.1778, + "lat": 51.5524, + "radius": 20, + "units": "mi", + }, + DialectVersion: 2, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 1 + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // STEP_END + + // STEP_START geo2 + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@pickup_zone:[CONTAINS $bike]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "bike": "POINT(-0.1278 51.5074)", + }, + DialectVersion: 3, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // STEP_END + + // STEP_START geo3 + res3, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@pickup_zone:[WITHIN $europe]", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "europe": "POLYGON((-25 35, 40 35, 40 70, -25 70, -25 35))", + }, + DialectVersion: 3, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + sort.Slice(res3.Docs, func(i, j int) bool { + return res3.Docs[i].ID < res3.Docs[j].ID + }) + + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // >>> bicycle:9 + // STEP_END + + // Output: + // 1 + // bicycle:5 + // 1 + // bicycle:5 + // 5 + // bicycle:5 + // bicycle:6 + // bicycle:7 + // bicycle:8 + // bicycle:9 +} From 1faab1f65759bca251c13ff458b4c7db2e1d6961 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:52:49 +0000 Subject: [PATCH 50/68] DOC-4300 added exact match examples (#3251) Co-authored-by: Nedyalko Dyakov --- doctests/query_em_test.go | 363 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 doctests/query_em_test.go diff --git a/doctests/query_em_test.go b/doctests/query_em_test.go new file mode 100644 index 000000000..d4267df4f --- /dev/null +++ b/doctests/query_em_test.go @@ -0,0 +1,363 @@ +// EXAMPLE: query_em +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_em() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START em1 + res1, err := rdb.FTSearch(ctx, + "idx:bicycle", "@price:[270 270]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 1 + + for _, doc := range res1.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:0 + + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "*", + &redis.FTSearchOptions{ + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: 270, + Max: 270, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 1 + + for _, doc := range res2.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:0 + // STEP_END + + // STEP_START em2 + res3, err := rdb.FTSearch(ctx, + "idx:bicycle", "@condition:{new}", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + for _, doc := range res3.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:5 + // >>> bicycle:0 + // >>> bicycle:6 + // >>> bicycle:7 + // >>> bicycle:8 + // STEP_END + + // STEP_START em3 + res4, err := rdb.FTCreate(ctx, + "idx:email", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"key:"}, + }, + &redis.FieldSchema{ + FieldName: "$.email", + As: "email", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4) // >>> OK + + res5, err := rdb.JSONSet(ctx, "key:1", "$", + map[string]interface{}{ + "email": "test@redis.com", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res5) // >>> OK + + res6, err := rdb.FTSearch(ctx, "idx:email", + "@email:{test\\@redis\\.com}", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res6.Total) // >>> 1 + // STEP_END + + // STEP_START em4 + res7, err := rdb.FTSearch(ctx, + "idx:bicycle", "@description:\"rough terrain\"", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res7.Total) // >>> 1 + + for _, doc := range res7.Docs { + fmt.Println(doc.ID) + } + // >>> bicycle:8 + // STEP_END + + // Output: + // 1 + // bicycle:0 + // 1 + // bicycle:0 + // 5 + // bicycle:5 + // bicycle:0 + // bicycle:6 + // bicycle:7 + // bicycle:8 + // OK + // OK + // 1 + // 1 + // bicycle:8 +} From 84cb9d27f2a77264fdb1a780f47d18c0c39407c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:02:15 +0200 Subject: [PATCH 51/68] chore(deps): bump rojopolis/spellcheck-github-actions (#3227) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.45.0 to 0.46.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.45.0...0.46.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 977f8c5c1..95cfdfaa0 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.45.0 + uses: rojopolis/spellcheck-github-actions@0.46.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 27f19ea672712866e59d80e09e8a9388d1116914 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 11:29:26 +0200 Subject: [PATCH 52/68] fix(aggregate, search): ft.aggregate bugfixes (#3263) * fix: rearange args for ft.aggregate apply should be before any groupby or sortby * improve test * wip: add scorer and addscores * enable all tests * fix ftsearch with count test * make linter happy * Addscores is available in later redisearch releases. For safety state it is available in redis ce 8 * load an apply seem to break scorer and addscores --- search_commands.go | 81 +++++++++++++++++++++++++---------- search_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 25 deletions(-) diff --git a/search_commands.go b/search_commands.go index 1312a78f0..df12bb3f9 100644 --- a/search_commands.go +++ b/search_commands.go @@ -240,13 +240,20 @@ type FTAggregateWithCursor struct { } type FTAggregateOptions struct { - Verbatim bool - LoadAll bool - Load []FTAggregateLoad - Timeout int - GroupBy []FTAggregateGroupBy - SortBy []FTAggregateSortBy - SortByMax int + Verbatim bool + LoadAll bool + Load []FTAggregateLoad + Timeout int + GroupBy []FTAggregateGroupBy + SortBy []FTAggregateSortBy + SortByMax int + // Scorer is used to set scoring function, if not set passed, a default will be used. + // The default scorer depends on the Redis version: + // - `BM25` for Redis >= 8 + // - `TFIDF` for Redis < 8 + Scorer string + // AddScores is available in Redis CE 8 + AddScores bool Apply []FTAggregateApply LimitOffset int Limit int @@ -490,6 +497,15 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } + + if options.Scorer != "" { + queryArgs = append(queryArgs, "SCORER", options.Scorer) + } + + if options.AddScores { + queryArgs = append(queryArgs, "ADDSCORES") + } + if options.LoadAll && options.Load != nil { panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } @@ -505,9 +521,18 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } } } + if options.Timeout > 0 { queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) } + + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } + } + if options.GroupBy != nil { for _, groupBy := range options.GroupBy { queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) @@ -549,12 +574,6 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.SortByMax > 0 { queryArgs = append(queryArgs, "MAX", options.SortByMax) } - for _, apply := range options.Apply { - queryArgs = append(queryArgs, "APPLY", apply.Field) - if apply.As != "" { - queryArgs = append(queryArgs, "AS", apply.As) - } - } if options.LimitOffset > 0 { queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) } @@ -581,6 +600,7 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery queryArgs = append(queryArgs, key, value) } } + if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) } @@ -661,11 +681,12 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { cmd.err = err - return nil + return err } cmd.val, err = ProcessAggregateResult(data) if err != nil { cmd.err = err + return err } return nil } @@ -681,6 +702,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Verbatim { args = append(args, "VERBATIM") } + if options.Scorer != "" { + args = append(args, "SCORER", options.Scorer) + } + if options.AddScores { + args = append(args, "ADDSCORES") + } if options.LoadAll && options.Load != nil { panic("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") } @@ -699,6 +726,12 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) } + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) + } + } if options.GroupBy != nil { for _, groupBy := range options.GroupBy { args = append(args, "GROUPBY", len(groupBy.Fields)) @@ -740,12 +773,6 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.SortByMax > 0 { args = append(args, "MAX", options.SortByMax) } - for _, apply := range options.Apply { - args = append(args, "APPLY", apply.Field) - if apply.As != "" { - args = append(args, "AS", apply.As) - } - } if options.LimitOffset > 0 { args = append(args, "LIMIT", options.LimitOffset) } @@ -1693,7 +1720,8 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { // FTSearch - Executes a search query on an index. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. -// For more information, please refer to the Redis documentation: +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// // [FT.SEARCH]: (https://redis.io/commands/ft.search/) func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} @@ -1704,6 +1732,12 @@ func (c cmdable) FTSearch(ctx context.Context, index string, query string) *FTSe type SearchQuery []interface{} +// FTSearchQuery - Executes a search query on an index with additional options. +// The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, +// and the 'options' parameter specifies additional options for the search. +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// +// [FT.SEARCH]: (https://redis.io/commands/ft.search/) func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { queryArgs := []interface{}{query} if options != nil { @@ -1816,7 +1850,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { // FTSearchWithArgs - Executes a search query on an index with additional options. // The 'index' parameter specifies the index to search, the 'query' parameter specifies the search query, // and the 'options' parameter specifies additional options for the search. -// For more information, please refer to the Redis documentation: +// For more information, please refer to the Redis documentation about [FT.SEARCH]. +// // [FT.SEARCH]: (https://redis.io/commands/ft.search/) func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query string, options *FTSearchOptions) *FTSearchCmd { args := []interface{}{"FT.SEARCH", index, query} @@ -1908,7 +1943,7 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin } } if options.SortByWithCount { - args = append(args, "WITHCOUT") + args = append(args, "WITHCOUNT") } } if options.LimitOffset >= 0 && options.Limit > 0 { diff --git a/search_test.go b/search_test.go index a409fc78a..e4e552152 100644 --- a/search_test.go +++ b/search_test.go @@ -2,6 +2,8 @@ package redis_test import ( "context" + "fmt" + "strconv" "time" . "github.com/bsm/ginkgo/v2" @@ -127,8 +129,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { res3, err := client.FTSearchWithArgs(ctx, "num", "foo", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() Expect(err).NotTo(HaveOccurred()) - Expect(res3.Total).To(BeEquivalentTo(int64(0))) + Expect(res3.Total).To(BeEquivalentTo(int64(3))) + res4, err := client.FTSearchWithArgs(ctx, "num", "notpresentf00", &redis.FTSearchOptions{NoContent: true, SortBy: []redis.FTSearchSortBy{sortBy2}, SortByWithCount: true}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res4.Total).To(BeEquivalentTo(int64(0))) }) It("should FTCreate and FTSearch example", Label("search", "ftcreate", "ftsearch"), func() { @@ -640,6 +645,100 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) }) + It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { + SkipBeforeRedisMajor(8, "ADDSCORES is available in Redis CE 8") + title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false} + description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "product:1", "title", "New Gaming Laptop", "description", "this is not a desktop") + client.HSet(ctx, "product:2", "title", "Super Old Not Gaming Laptop", "description", "this laptop is not a new laptop but it is a laptop") + client.HSet(ctx, "product:3", "title", "Office PC", "description", "office desktop pc") + + options := &redis.FTAggregateOptions{ + AddScores: true, + Scorer: "BM25", + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@__score", + Desc: true, + }}, + } + + res, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Rows)).To(BeEquivalentTo(2)) + score1, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[0].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + score2, err := strconv.ParseFloat(fmt.Sprintf("%s", res.Rows[1].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + Expect(score1).To(BeNumerically(">", score2)) + + optionsDM := &redis.FTAggregateOptions{ + AddScores: true, + Scorer: "DISMAX", + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@__score", + Desc: true, + }}, + } + + resDM, err := client.FTAggregateWithArgs(ctx, "idx1", "laptop", optionsDM).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resDM).ToNot(BeNil()) + Expect(len(resDM.Rows)).To(BeEquivalentTo(2)) + score1DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[0].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + score2DM, err := strconv.ParseFloat(fmt.Sprintf("%s", resDM.Rows[1].Fields["__score"]), 64) + Expect(err).NotTo(HaveOccurred()) + Expect(score1DM).To(BeNumerically(">", score2DM)) + + Expect(score1DM).To(BeEquivalentTo(float64(4))) + Expect(score2DM).To(BeEquivalentTo(float64(1))) + Expect(score1).NotTo(BeEquivalentTo(score1DM)) + Expect(score2).NotTo(BeEquivalentTo(score2DM)) + }) + + It("should FTAggregate apply and groupby", Label("search", "ftaggregate"), func() { + text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} + num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} + val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1, num1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // 6 feb + client.HSet(ctx, "doc1", "PrimaryKey", "9::362330", "CreatedDateTimeUTC", "1738823999") + + // 12 feb + client.HSet(ctx, "doc2", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739342399") + client.HSet(ctx, "doc3", "PrimaryKey", "9::362329", "CreatedDateTimeUTC", "1739353199") + + reducer := redis.FTAggregateReducer{Reducer: redis.SearchCount, As: "perDay"} + + options := &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{{Field: "floor(@CreatedDateTimeUTC /(60*60*24))", As: "TimestampAsDay"}}, + GroupBy: []redis.FTAggregateGroupBy{{ + Fields: []interface{}{"@TimestampAsDay"}, + Reduce: []redis.FTAggregateReducer{reducer}, + }}, + SortBy: []redis.FTAggregateSortBy{{ + FieldName: "@perDay", + Desc: true, + }}, + } + + res, err := client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).ToNot(BeNil()) + Expect(len(res.Rows)).To(BeEquivalentTo(2)) + Expect(res.Rows[0].Fields["perDay"]).To(BeEquivalentTo("2")) + Expect(res.Rows[1].Fields["perDay"]).To(BeEquivalentTo("1")) + }) + It("should FTAggregate apply", Label("search", "ftaggregate"), func() { text1 := &redis.FieldSchema{FieldName: "PrimaryKey", FieldType: redis.SearchFieldTypeText, Sortable: true} num1 := &redis.FieldSchema{FieldName: "CreatedDateTimeUTC", FieldType: redis.SearchFieldTypeNumeric, Sortable: true} @@ -684,7 +783,6 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Rows[0].Fields["age"]).To(BeEquivalentTo("19")) Expect(res.Rows[1].Fields["age"]).To(BeEquivalentTo("25")) } - }) It("should FTSearch SkipInitialScan", Label("search", "ftsearch"), func() { From d0f921357d9751a3e11221a7abd072546ac67654 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 12:09:49 +0200 Subject: [PATCH 53/68] fix: add unstableresp3 to cluster client (#3266) * fix: add unstableresp3 to cluster client * propagate unstableresp3 * proper test that will ignore error, but fail if client panics * add separate test for clusterclient constructor --- options.go | 2 +- osscluster.go | 6 +++++- universal.go | 1 + universal_test.go | 22 ++++++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/options.go b/options.go index b9701702f..a350a02f9 100644 --- a/options.go +++ b/options.go @@ -154,7 +154,7 @@ type Options struct { // Add suffix to client name. Default is empty. IdentitySuffix string - // Enable Unstable mode for Redis Search module with RESP3. + // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. UnstableResp3 bool } diff --git a/osscluster.go b/osscluster.go index 188f50359..517fbd450 100644 --- a/osscluster.go +++ b/osscluster.go @@ -94,6 +94,9 @@ type ClusterOptions struct { DisableIndentity bool // Disable set-lib on connect. Default is false. IdentitySuffix string // Add suffix to client name. Default is empty. + + // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. + UnstableResp3 bool } func (opt *ClusterOptions) init() { @@ -308,7 +311,8 @@ func (opt *ClusterOptions) clientOptions() *Options { // much use for ClusterSlots config). This means we cannot execute the // READONLY command against that node -- setting readOnly to false in such // situations in the options below will prevent that from happening. - readOnly: opt.ReadOnly && opt.ClusterSlots == nil, + readOnly: opt.ReadOnly && opt.ClusterSlots == nil, + UnstableResp3: opt.UnstableResp3, } } diff --git a/universal.go b/universal.go index f4d2d7598..47fda2769 100644 --- a/universal.go +++ b/universal.go @@ -115,6 +115,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, + UnstableResp3: o.UnstableResp3, } } diff --git a/universal_test.go b/universal_test.go index 747c68acb..9328b4776 100644 --- a/universal_test.go +++ b/universal_test.go @@ -38,4 +38,26 @@ var _ = Describe("UniversalClient", func() { }) Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) }) + + It("connect to clusters with UniversalClient and UnstableResp3", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: cluster.addrs(), + Protocol: 3, + UnstableResp3: true, + }) + Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) + a := func() { client.FTInfo(ctx, "all").Result() } + Expect(a).ToNot(Panic()) + }) + + It("connect to clusters with ClusterClient and UnstableResp3", Label("NonRedisEnterprise"), func() { + client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: cluster.addrs(), + Protocol: 3, + UnstableResp3: true, + }) + Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) + a := func() { client.FTInfo(ctx, "all").Result() } + Expect(a).ToNot(Panic()) + }) }) From 7f8b5a81675f7f6099f365bb769dd2b1a378ac9a Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 7 Feb 2025 12:56:16 +0200 Subject: [PATCH 54/68] fix: flaky ClientKillByFilter test (#3268) --- commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands_test.go b/commands_test.go index 404ffd02b..829a94b65 100644 --- a/commands_test.go +++ b/commands_test.go @@ -217,7 +217,7 @@ var _ = Describe("Commands", func() { killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)), Equal(int64(4)))) + Expect(killed.Val()).To(BeNumerically(">=", 2)) select { case <-done: From 71311d804488d8ec834a7c4ff8fef87e6c6e9175 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:22:43 +0000 Subject: [PATCH 55/68] DOC-4335 added aggregate query examples (#3259) * DOC-4335 added aggregate query examples (demonstrating errors) * DOC-4335 remove incomplete examples * DOC-4335 added missing examples using latest client updates --------- Co-authored-by: Nedyalko Dyakov --- doctests/query_agg_test.go | 433 +++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 doctests/query_agg_test.go diff --git a/doctests/query_agg_test.go b/doctests/query_agg_test.go new file mode 100644 index 000000000..a710087e4 --- /dev/null +++ b/doctests/query_agg_test.go @@ -0,0 +1,433 @@ +// EXAMPLE: query_agg +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + "sort" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_agg() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START agg1 + res1, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", + "@condition:{new}", + &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{ + { + Field: "@price - (@price * 0.1)", + As: "discounted", + }, + }, + Load: []redis.FTAggregateLoad{ + {Field: "__key"}, + {Field: "price"}, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res1.Rows)) // >>> 5 + + sort.Slice(res1.Rows, func(i, j int) bool { + return res1.Rows[i].Fields["__key"].(string) < + res1.Rows[j].Fields["__key"].(string) + }) + + for _, row := range res1.Rows { + fmt.Printf( + "__key=%v, discounted=%v, price=%v\n", + row.Fields["__key"], + row.Fields["discounted"], + row.Fields["price"], + ) + } + // >>> __key=bicycle:0, discounted=243, price=270 + // >>> __key=bicycle:5, discounted=729, price=810 + // >>> __key=bicycle:6, discounted=2070, price=2300 + // >>> __key=bicycle:7, discounted=387, price=430 + // >>> __key=bicycle:8, discounted=1080, price=1200 + // STEP_END + + // STEP_START agg2 + res2, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{ + {Field: "price"}, + }, + Apply: []redis.FTAggregateApply{ + { + Field: "@price<1000", + As: "price_category", + }, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@condition"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchSum, + Args: []interface{}{"@price_category"}, + As: "num_affordable", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res2.Rows)) // >>> 3 + + sort.Slice(res2.Rows, func(i, j int) bool { + return res2.Rows[i].Fields["condition"].(string) < + res2.Rows[j].Fields["condition"].(string) + }) + + for _, row := range res2.Rows { + fmt.Printf( + "condition=%v, num_affordable=%v\n", + row.Fields["condition"], + row.Fields["num_affordable"], + ) + } + // >>> condition=new, num_affordable=3 + // >>> condition=refurbished, num_affordable=1 + // >>> condition=used, num_affordable=1 + // STEP_END + + // STEP_START agg3 + + res3, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Apply: []redis.FTAggregateApply{ + { + Field: "'bicycle'", + As: "type", + }, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@type"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchCount, + As: "num_total", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res3.Rows)) // >>> 1 + + for _, row := range res3.Rows { + fmt.Printf( + "type=%v, num_total=%v\n", + row.Fields["type"], + row.Fields["num_total"], + ) + } + // type=bicycle, num_total=10 + // STEP_END + + // STEP_START agg4 + res4, err := rdb.FTAggregateWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTAggregateOptions{ + Load: []redis.FTAggregateLoad{ + {Field: "__key"}, + }, + GroupBy: []redis.FTAggregateGroupBy{ + { + Fields: []interface{}{"@condition"}, + Reduce: []redis.FTAggregateReducer{ + { + Reducer: redis.SearchToList, + Args: []interface{}{"__key"}, + As: "bicycles", + }, + }, + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(res4.Rows)) // >>> 3 + + sort.Slice(res4.Rows, func(i, j int) bool { + return res4.Rows[i].Fields["condition"].(string) < + res4.Rows[j].Fields["condition"].(string) + }) + + for _, row := range res4.Rows { + rowBikes := row.Fields["bicycles"].([]interface{}) + bikes := make([]string, len(rowBikes)) + + for i, rowBike := range rowBikes { + bikes[i] = rowBike.(string) + } + + sort.Slice(bikes, func(i, j int) bool { + return bikes[i] < bikes[j] + }) + + fmt.Printf( + "condition=%v, bicycles=%v\n", + row.Fields["condition"], + bikes, + ) + } + // >>> condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8] + // >>> condition=refurbished, bicycles=[bicycle:9] + // >>> condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4] + // STEP_END + + // Output: + // 5 + // __key=bicycle:0, discounted=243, price=270 + // __key=bicycle:5, discounted=729, price=810 + // __key=bicycle:6, discounted=2070, price=2300 + // __key=bicycle:7, discounted=387, price=430 + // __key=bicycle:8, discounted=1080, price=1200 + // 3 + // condition=new, num_affordable=3 + // condition=refurbished, num_affordable=1 + // condition=used, num_affordable=1 + // 1 + // type=bicycle, num_total=10 + // 3 + // condition=new, bicycles=[bicycle:0 bicycle:5 bicycle:6 bicycle:7 bicycle:8] + // condition=refurbished, bicycles=[bicycle:9] + // condition=used, bicycles=[bicycle:1 bicycle:2 bicycle:3 bicycle:4] +} From acbf4a688fbd6d72c0c8f04d61a7326ce664a0be Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 10 Feb 2025 14:55:15 +0200 Subject: [PATCH 56/68] test: add test for `info` in RCE 8 (#3269) --- commands_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/commands_test.go b/commands_test.go index 829a94b65..ff48cfce5 100644 --- a/commands_test.go +++ b/commands_test.go @@ -532,6 +532,59 @@ var _ = Describe("Commands", func() { Expect(info.Val()).To(HaveLen(1)) }) + It("should Info Modules", Label("redis.info"), func() { + SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + info := client.Info(ctx) + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).NotTo(BeNil()) + + info = client.Info(ctx, "search") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + + info = client.Info(ctx, "modules") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + Expect(info.Val()).To(ContainSubstring("ReJSON")) + Expect(info.Val()).To(ContainSubstring("timeseries")) + Expect(info.Val()).To(ContainSubstring("bf")) + + info = client.Info(ctx, "everything") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).To(ContainSubstring("search")) + Expect(info.Val()).To(ContainSubstring("ReJSON")) + Expect(info.Val()).To(ContainSubstring("timeseries")) + Expect(info.Val()).To(ContainSubstring("bf")) + }) + + It("should InfoMap Modules", Label("redis.info"), func() { + SkipBeforeRedisMajor(8, "modules are included in info for Redis Version >= 8") + info := client.InfoMap(ctx) + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(info.Val()).NotTo(BeNil()) + + info = client.InfoMap(ctx, "search") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(len(info.Val())).To(BeNumerically(">=", 2)) + Expect(info.Val()["search_version"]).ToNot(BeNil()) + + info = client.InfoMap(ctx, "modules") + Expect(info.Err()).NotTo(HaveOccurred()) + val := info.Val() + modules, ok := val["Modules"] + Expect(ok).To(BeTrue()) + Expect(len(val)).To(BeNumerically(">=", 2)) + Expect(val["search_version"]).ToNot(BeNil()) + Expect(modules["search"]).ToNot(BeNil()) + Expect(modules["ReJSON"]).ToNot(BeNil()) + Expect(modules["timeseries"]).ToNot(BeNil()) + Expect(modules["bf"]).ToNot(BeNil()) + + info = client.InfoMap(ctx, "everything") + Expect(info.Err()).NotTo(HaveOccurred()) + Expect(len(info.Val())).To(BeNumerically(">=", 10)) + }) + It("should Info cpu", func() { info := client.Info(ctx, "cpu") Expect(info.Err()).NotTo(HaveOccurred()) From 021faad4faec0f99b02dcf4bab67e34dc9738ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Mon, 10 Feb 2025 20:55:40 +0800 Subject: [PATCH 57/68] test(redisotel): rename redisotel_test.go to tracing_test.go and add tracing hook tests (#3270) Co-authored-by: Nedyalko Dyakov --- extra/redisotel/go.mod | 6 +- extra/redisotel/go.sum | 4 +- extra/redisotel/redisotel_test.go | 61 -------- extra/redisotel/tracing_test.go | 240 ++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 67 deletions(-) delete mode 100644 extra/redisotel/redisotel_test.go create mode 100644 extra/redisotel/tracing_test.go diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index b2e30b394..47aab0db1 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -16,13 +16,11 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect golang.org/x/sys v0.16.0 // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/redisotel/go.sum b/extra/redisotel/go.sum index 9eb9bcd4e..4b832c80f 100644 --- a/extra/redisotel/go.sum +++ b/extra/redisotel/go.sum @@ -1,7 +1,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/extra/redisotel/redisotel_test.go b/extra/redisotel/redisotel_test.go deleted file mode 100644 index b1ad5ca43..000000000 --- a/extra/redisotel/redisotel_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package redisotel - -import ( - "context" - "testing" - - semconv "go.opentelemetry.io/otel/semconv/v1.7.0" - - "go.opentelemetry.io/otel" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" - - "github.com/redis/go-redis/v9" -) - -type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider - -func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider { - return fn(name, opts...) -} - -func TestNewWithTracerProvider(t *testing.T) { - invoked := false - - tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider { - invoked = true - return otel.GetTracerProvider() - }) - - _ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test"))) - - if !invoked { - t.Fatalf("did not call custom TraceProvider") - } -} - -func TestWithDBStatement(t *testing.T) { - provider := sdktrace.NewTracerProvider() - hook := newTracingHook( - "", - WithTracerProvider(provider), - WithDBStatement(false), - ) - ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") - cmd := redis.NewCmd(ctx, "ping") - defer span.End() - - processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { - attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() - for _, attr := range attrs { - if attr.Key == semconv.DBStatementKey { - t.Fatal("Attribute with db statement should not exist") - } - } - return nil - }) - err := processHook(ctx, cmd) - if err != nil { - t.Fatal(err) - } -} diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go new file mode 100644 index 000000000..bbe828144 --- /dev/null +++ b/extra/redisotel/tracing_test.go @@ -0,0 +1,240 @@ +package redisotel + +import ( + "context" + "fmt" + "net" + "testing" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.7.0" + + "go.opentelemetry.io/otel" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/redis/go-redis/v9" +) + +type providerFunc func(name string, opts ...trace.TracerOption) trace.TracerProvider + +func (fn providerFunc) TracerProvider(name string, opts ...trace.TracerOption) trace.TracerProvider { + return fn(name, opts...) +} + +func TestNewWithTracerProvider(t *testing.T) { + invoked := false + + tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.TracerProvider { + invoked = true + return otel.GetTracerProvider() + }) + + _ = newTracingHook("redis-hook", WithTracerProvider(tp.TracerProvider("redis-test"))) + + if !invoked { + t.Fatalf("did not call custom TraceProvider") + } +} + +func TestWithDBStatement(t *testing.T) { + provider := sdktrace.NewTracerProvider() + hook := newTracingHook( + "", + WithTracerProvider(provider), + WithDBStatement(false), + ) + ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") + cmd := redis.NewCmd(ctx, "ping") + defer span.End() + + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() + for _, attr := range attrs { + if attr.Key == semconv.DBStatementKey { + t.Fatal("Attribute with db statement should not exist") + } + } + return nil + }) + err := processHook(ctx, cmd) + if err != nil { + t.Fatal(err) + } +} + +func TestTracingHook_DialHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + dialHook := hook.DialHook(func(ctx context.Context, network, addr string) (conn net.Conn, err error) { + return nil, tt.errTest + }) + if _, err := dialHook(context.Background(), "tcp", "localhost:6379"); err != tt.errTest { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "redis.dial", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func TestTracingHook_ProcessHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + cmd := redis.NewCmd(context.Background(), "ping") + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + return tt.errTest + }) + assertEqual(t, tt.errTest, processHook(context.Background(), cmd)) + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "ping", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func TestTracingHook_ProcessPipelineHook(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + errTest error + }{ + {"nil error", nil}, + {"test error", fmt.Errorf("test error")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + cmds := []redis.Cmder{ + redis.NewCmd(context.Background(), "ping"), + redis.NewCmd(context.Background(), "ping"), + } + processHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error { + return tt.errTest + }) + assertEqual(t, tt.errTest, processHook(context.Background(), cmds)) + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + assertEqual(t, instrumName, spanData.InstrumentationLibrary.Name) + assertEqual(t, "redis.pipeline ping", spanData.Name) + assertEqual(t, trace.SpanKindClient, spanData.SpanKind) + assertAttributeContains(t, spanData.Attributes, semconv.DBSystemRedis) + assertAttributeContains(t, spanData.Attributes, semconv.DBConnectionStringKey.String("redis://localhost:6379")) + assertAttributeContains(t, spanData.Attributes, semconv.DBStatementKey.String("ping\nping")) + + if tt.errTest == nil { + assertEqual(t, 0, len(spanData.Events)) + assertEqual(t, codes.Unset, spanData.Status.Code) + assertEqual(t, "", spanData.Status.Description) + return + } + + assertEqual(t, 1, len(spanData.Events)) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionTypeKey.String("*errors.errorString")) + assertAttributeContains(t, spanData.Events[0].Attributes, semconv.ExceptionMessageKey.String(tt.errTest.Error())) + assertEqual(t, codes.Error, spanData.Status.Code) + assertEqual(t, tt.errTest.Error(), spanData.Status.Description) + }) + } +} + +func assertEqual(t *testing.T, expected, actual interface{}) { + t.Helper() + if expected != actual { + t.Fatalf("expected %v, got %v", expected, actual) + } +} + +func assertAttributeContains(t *testing.T, attrs []attribute.KeyValue, attr attribute.KeyValue) { + t.Helper() + for _, a := range attrs { + if a == attr { + return + } + } + t.Fatalf("attribute %v not found", attr) +} From 40e049e67a4cce574f7972b53b1a8172bbe45d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Mon, 10 Feb 2025 21:03:50 +0800 Subject: [PATCH 58/68] chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 (#3265) * chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 * chore(deps): update github.com/cespare/xxhash/v2 to v2.3.0 --- example/del-keys-without-ttl/go.mod | 2 +- example/del-keys-without-ttl/go.sum | 4 ++-- example/hll/go.mod | 2 +- example/hll/go.sum | 4 ++-- example/lua-scripting/go.mod | 2 +- example/lua-scripting/go.sum | 4 ++-- example/otel/go.mod | 2 +- example/otel/go.sum | 4 ++-- example/redis-bloom/go.mod | 2 +- example/redis-bloom/go.sum | 4 ++-- example/scan-struct/go.mod | 2 +- example/scan-struct/go.sum | 4 ++-- extra/rediscensus/go.mod | 6 ++---- extra/rediscensus/go.sum | 4 ++-- extra/rediscmd/go.mod | 6 ++---- extra/rediscmd/go.sum | 4 ++-- extra/redisprometheus/go.mod | 6 ++---- extra/redisprometheus/go.sum | 4 ++-- 18 files changed, 30 insertions(+), 36 deletions(-) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index d725db0bb..9290eb039 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -10,7 +10,7 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect diff --git a/example/del-keys-without-ttl/go.sum b/example/del-keys-without-ttl/go.sum index e426a762b..96beed56e 100644 --- a/example/del-keys-without-ttl/go.sum +++ b/example/del-keys-without-ttl/go.sum @@ -1,8 +1,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/hll/go.mod b/example/hll/go.mod index 7093be420..e2bf03d92 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/hll/go.sum b/example/hll/go.sum index 0e92df5e7..d64ea0303 100644 --- a/example/hll/go.sum +++ b/example/hll/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 85a82860a..5c811bf25 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/lua-scripting/go.sum b/example/lua-scripting/go.sum index 0e92df5e7..d64ea0303 100644 --- a/example/lua-scripting/go.sum +++ b/example/lua-scripting/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/otel/go.mod b/example/otel/go.mod index 3f1d858e1..f5e2a156c 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -17,7 +17,7 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index e85481dbe..1a1729c6e 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -2,8 +2,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 3825432a7..9076e1474 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -7,6 +7,6 @@ replace github.com/redis/go-redis/v9 => ../.. require github.com/redis/go-redis/v9 v9.6.2 require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/redis-bloom/go.sum b/example/redis-bloom/go.sum index 0e92df5e7..d64ea0303 100644 --- a/example/redis-bloom/go.sum +++ b/example/redis-bloom/go.sum @@ -1,6 +1,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index fca1a5972..f14f54df1 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -10,6 +10,6 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/example/scan-struct/go.sum b/example/scan-struct/go.sum index 6274a65f6..5496d29e5 100644 --- a/example/scan-struct/go.sum +++ b/example/scan-struct/go.sum @@ -1,7 +1,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index bae3f7b93..a28ad7dfd 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -13,11 +13,9 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/rediscensus/go.sum b/extra/rediscensus/go.sum index cf8f90721..ab3a8984f 100644 --- a/extra/rediscensus/go.sum +++ b/extra/rediscensus/go.sum @@ -3,8 +3,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 594cfdf1e..07df0cc2d 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -11,10 +11,8 @@ require ( ) require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/rediscmd/go.sum b/extra/rediscmd/go.sum index 21b4f64ee..4db68f6d4 100644 --- a/extra/rediscmd/go.sum +++ b/extra/rediscmd/go.sum @@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 5cbafac11..42a6f805c 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -11,7 +11,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -22,6 +22,4 @@ require ( google.golang.org/protobuf v1.33.0 // indirect ) -retract ( - v9.5.3 // This version was accidentally released. -) +retract v9.5.3 // This version was accidentally released. diff --git a/extra/redisprometheus/go.sum b/extra/redisprometheus/go.sum index 6528a5e33..7093016ee 100644 --- a/extra/redisprometheus/go.sum +++ b/extra/redisprometheus/go.sum @@ -2,8 +2,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= From 9db1286414cf56749e03b5ea23ef06f71b58e09a Mon Sep 17 00:00:00 2001 From: LINKIWI Date: Tue, 11 Feb 2025 07:50:31 -0800 Subject: [PATCH 59/68] Reinstate read-only lock on hooks access in dialHook (#3225) --- redis.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/redis.go b/redis.go index 2f576bdbe..ec3ff616a 100644 --- a/redis.go +++ b/redis.go @@ -41,7 +41,7 @@ type ( ) type hooksMixin struct { - hooksMu *sync.Mutex + hooksMu *sync.RWMutex slice []Hook initial hooks @@ -49,7 +49,7 @@ type hooksMixin struct { } func (hs *hooksMixin) initHooks(hooks hooks) { - hs.hooksMu = new(sync.Mutex) + hs.hooksMu = new(sync.RWMutex) hs.initial = hooks hs.chain() } @@ -151,7 +151,7 @@ func (hs *hooksMixin) clone() hooksMixin { clone := *hs l := len(clone.slice) clone.slice = clone.slice[:l:l] - clone.hooksMu = new(sync.Mutex) + clone.hooksMu = new(sync.RWMutex) return clone } @@ -176,7 +176,14 @@ func (hs *hooksMixin) withProcessPipelineHook( } func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) { - return hs.current.dial(ctx, network, addr) + // Access to hs.current is guarded by a read-only lock since it may be mutated by AddHook(...) + // while this dialer is concurrently accessed by the background connection pool population + // routine when MinIdleConns > 0. + hs.hooksMu.RLock() + current := hs.current + hs.hooksMu.RUnlock() + + return current.dial(ctx, network, addr) } func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error { From 196fc9b21ac460f0d5251d349e4249e2ffedb9ff Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Fri, 14 Feb 2025 13:07:39 +0200 Subject: [PATCH 60/68] use limit when limitoffset is zero (#3275) --- search_commands.go | 14 ++++---------- search_test.go | 5 +++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/search_commands.go b/search_commands.go index df12bb3f9..878f874ef 100644 --- a/search_commands.go +++ b/search_commands.go @@ -574,11 +574,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.SortByMax > 0 { queryArgs = append(queryArgs, "MAX", options.SortByMax) } - if options.LimitOffset > 0 { - queryArgs = append(queryArgs, "LIMIT", options.LimitOffset) - } - if options.Limit > 0 { - queryArgs = append(queryArgs, options.Limit) + if options.LimitOffset >= 0 && options.Limit > 0 { + queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) } if options.Filter != "" { queryArgs = append(queryArgs, "FILTER", options.Filter) @@ -773,11 +770,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.SortByMax > 0 { args = append(args, "MAX", options.SortByMax) } - if options.LimitOffset > 0 { - args = append(args, "LIMIT", options.LimitOffset) - } - if options.Limit > 0 { - args = append(args, options.Limit) + if options.LimitOffset >= 0 && options.Limit > 0 { + args = append(args, "LIMIT", options.LimitOffset, options.Limit) } if options.Filter != "" { args = append(args, "FILTER", options.Filter) diff --git a/search_test.go b/search_test.go index e4e552152..993116da9 100644 --- a/search_test.go +++ b/search_test.go @@ -616,6 +616,11 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("b")) + + options = &redis.FTAggregateOptions{SortBy: []redis.FTAggregateSortBy{{FieldName: "@t1"}}, Limit: 1, LimitOffset: 0} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("a")) }) It("should FTAggregate load ", Label("search", "ftaggregate"), func() { From a17250d6bc51d76a884406a405f9febd628f930e Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Tue, 18 Feb 2025 13:51:22 +0200 Subject: [PATCH 61/68] fix: linter configuration (#3279) --- .golangci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index de514554a..285aca6b3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,3 @@ run: - concurrency: 8 - deadline: 5m + timeout: 5m tests: false From c29d399be6e6bd1940835228a5cdd283f8c5c9d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:04:03 +0200 Subject: [PATCH 62/68] chore(deps): bump rojopolis/spellcheck-github-actions (#3276) Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.46.0 to 0.47.0. - [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases) - [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md) - [Commits](https://github.com/rojopolis/spellcheck-github-actions/compare/0.46.0...0.47.0) --- updated-dependencies: - dependency-name: rojopolis/spellcheck-github-actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- .github/workflows/spellcheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index 95cfdfaa0..beefa6164 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -8,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Check Spelling - uses: rojopolis/spellcheck-github-actions@0.46.0 + uses: rojopolis/spellcheck-github-actions@0.47.0 with: config_path: .github/spellcheck-settings.yml task_name: Markdown From 7e517ec4a15eb22c60591a96e8a908c0dae40a3b Mon Sep 17 00:00:00 2001 From: herodot <54836727+bitsark@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:55:49 +0800 Subject: [PATCH 63/68] fix(search&aggregate):fix error overwrite and typo #3220 (#3224) * fix (#3220) * LOAD has NO AS param(https://redis.io/docs/latest/commands/ft.aggregate/) * fix typo: WITHCOUT -> WITHCOUNT * fix (#3220): * Compatible with known RediSearch issue in test * fix (#3220) * fixed the calculation bug of the count of load params * test should not include special condition * return errors when they occur --------- Co-authored-by: Nedyalko Dyakov Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- search_commands.go | 22 +++++++++++++--------- search_test.go | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/search_commands.go b/search_commands.go index 878f874ef..71ee6ab32 100644 --- a/search_commands.go +++ b/search_commands.go @@ -514,12 +514,16 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery } if options.Load != nil { queryArgs = append(queryArgs, "LOAD", len(options.Load)) + index, count := len(queryArgs)-1, 0 for _, load := range options.Load { queryArgs = append(queryArgs, load.Field) + count++ if load.As != "" { queryArgs = append(queryArgs, "AS", load.As) + count += 2 } } + queryArgs[index] = count } if options.Timeout > 0 { @@ -677,12 +681,10 @@ func (cmd *AggregateCmd) String() string { func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err return err } cmd.val, err = ProcessAggregateResult(data) if err != nil { - cmd.err = err return err } return nil @@ -713,12 +715,16 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st } if options.Load != nil { args = append(args, "LOAD", len(options.Load)) + index, count := len(args)-1, 0 for _, load := range options.Load { args = append(args, load.Field) + count++ if load.As != "" { args = append(args, "AS", load.As) + count += 2 } } + args[index] = count } if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) @@ -1420,7 +1426,7 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { } cmd.val, err = parseFTInfo(data) if err != nil { - cmd.err = err + return err } return nil @@ -1513,12 +1519,11 @@ func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) { func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err - return nil + return err } cmd.val, err = parseFTSpellCheck(data) if err != nil { - cmd.err = err + return err } return nil } @@ -1702,12 +1707,11 @@ func (cmd *FTSearchCmd) RawResult() (interface{}, error) { func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { data, err := rd.ReadSlice() if err != nil { - cmd.err = err - return nil + return err } cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) if err != nil { - cmd.err = err + return err } return nil } diff --git a/search_test.go b/search_test.go index 993116da9..ea3460d3d 100644 --- a/search_test.go +++ b/search_test.go @@ -269,6 +269,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(BeEquivalentTo(int64(1))) + _, err = client.FTSearch(ctx, "idx_not_exist", "only in the body").Result() + Expect(err).To(HaveOccurred()) }) It("should FTSpellCheck", Label("search", "ftcreate", "ftsearch", "ftspellcheck"), func() { @@ -643,11 +645,25 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t2", As: "t2alias"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world")) + + options = &redis.FTAggregateOptions{Load: []redis.FTAggregateLoad{{Field: "t1"}, {Field: "t2", As: "t2alias"}}} + res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) + Expect(res.Rows[0].Fields["t2alias"]).To(BeEquivalentTo("world")) + options = &redis.FTAggregateOptions{LoadAll: true} res, err = client.FTAggregateWithArgs(ctx, "idx1", "*", options).Result() Expect(err).NotTo(HaveOccurred()) Expect(res.Rows[0].Fields["t1"]).To(BeEquivalentTo("hello")) Expect(res.Rows[0].Fields["t2"]).To(BeEquivalentTo("world")) + + _, err = client.FTAggregateWithArgs(ctx, "idx_not_exist", "*", &redis.FTAggregateOptions{}).Result() + Expect(err).To(HaveOccurred()) }) It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { @@ -1268,6 +1284,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { val, err = client.FTCreate(ctx, "idx_hash", ftCreateOptions, schema...).Result() Expect(err).NotTo(HaveOccurred()) Expect(val).To(Equal("OK")) + WaitForIndexing(client, "idx_hash") ftSearchOptions := &redis.FTSearchOptions{ DialectVersion: 4, From f3c2711fe1e5f8402384f61a817a637b5cd42099 Mon Sep 17 00:00:00 2001 From: alingse Date: Thu, 20 Feb 2025 22:22:34 +0800 Subject: [PATCH 64/68] move regexp.MustCompile close to call (#3280) * move regexp.MustCompile out of func * move moduleRe close to call --------- Co-authored-by: Nedyalko Dyakov --- command.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/command.go b/command.go index 2623a2396..696501453 100644 --- a/command.go +++ b/command.go @@ -5492,8 +5492,6 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error { section := "" scanner := bufio.NewScanner(strings.NewReader(val)) - moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`) - for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") { @@ -5504,6 +5502,7 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error { cmd.val[section] = make(map[string]string) } else if line != "" { if section == "Modules" { + moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`) kv := moduleRe.FindStringSubmatch(line) if len(kv) == 3 { cmd.val[section][kv[1]] = kv[2] From 747190e2311ccb57c76a9b0f2c58c79674b33561 Mon Sep 17 00:00:00 2001 From: andy-stark-redis <164213578+andy-stark-redis@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:23:05 +0000 Subject: [PATCH 65/68] DOC-4329 added range query examples (#3252) Co-authored-by: Nedyalko Dyakov --- doctests/query_range_test.go | 376 +++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 doctests/query_range_test.go diff --git a/doctests/query_range_test.go b/doctests/query_range_test.go new file mode 100644 index 000000000..41438ff0e --- /dev/null +++ b/doctests/query_range_test.go @@ -0,0 +1,376 @@ +// EXAMPLE: query_range +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +func ExampleClient_query_range() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + + // HIDE_END + // REMOVE_START + rdb.FTDropIndex(ctx, "idx:bicycle") + rdb.FTDropIndex(ctx, "idx:email") + // REMOVE_END + + _, err := rdb.FTCreate(ctx, "idx:bicycle", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"bicycle:"}, + }, + &redis.FieldSchema{ + FieldName: "$.brand", + As: "brand", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.model", + As: "model", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.description", + As: "description", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.price", + As: "price", + FieldType: redis.SearchFieldTypeNumeric, + }, + &redis.FieldSchema{ + FieldName: "$.condition", + As: "condition", + FieldType: redis.SearchFieldTypeTag, + }, + ).Result() + + if err != nil { + panic(err) + } + + exampleJsons := []map[string]interface{}{ + { + "pickup_zone": "POLYGON((-74.0610 40.7578, -73.9510 40.7578, -73.9510 40.6678, " + + "-74.0610 40.6678, -74.0610 40.7578))", + "store_location": "-74.0060,40.7128", + "brand": "Velorim", + "model": "Jigger", + "price": 270, + "description": "Small and powerful, the Jigger is the best ride for the smallest of tikes! " + + "This is the tiniest kids pedal bike on the market available without a coaster brake, the Jigger " + + "is the vehicle of choice for the rare tenacious little rider raring to go.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((-118.2887 34.0972, -118.1987 34.0972, -118.1987 33.9872, " + + "-118.2887 33.9872, -118.2887 34.0972))", + "store_location": "-118.2437,34.0522", + "brand": "Bicyk", + "model": "Hillcraft", + "price": 1200, + "description": "Kids want to ride with as little weight as possible. Especially " + + "on an incline! They may be at the age when a 27.5'' wheel bike is just too clumsy coming " + + "off a 24'' bike. The Hillcraft 26 is just the solution they need!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-87.6848 41.9331, -87.5748 41.9331, -87.5748 41.8231, " + + "-87.6848 41.8231, -87.6848 41.9331))", + "store_location": "-87.6298,41.8781", + "brand": "Nord", + "model": "Chook air 5", + "price": 815, + "description": "The Chook Air 5 gives kids aged six years and older a durable " + + "and uberlight mountain bike for their first experience on tracks and easy cruising through " + + "forests and fields. The lower top tube makes it easy to mount and dismount in any " + + "situation, giving your kids greater safety on the trails.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-80.2433 25.8067, -80.1333 25.8067, -80.1333 25.6967, " + + "-80.2433 25.6967, -80.2433 25.8067))", + "store_location": "-80.1918,25.7617", + "brand": "Eva", + "model": "Eva 291", + "price": 3400, + "description": "The sister company to Nord, Eva launched in 2005 as the first " + + "and only women-dedicated bicycle brand. Designed by women for women, allEva bikes " + + "are optimized for the feminine physique using analytics from a body metrics database. " + + "If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This " + + "full-suspension, cross-country ride has been designed for velocity. The 291 has " + + "100mm of front and rear travel, a superlight aluminum frame and fast-rolling " + + "29-inch wheels. Yippee!", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-122.4644 37.8199, -122.3544 37.8199, -122.3544 37.7099, " + + "-122.4644 37.7099, -122.4644 37.8199))", + "store_location": "-122.4194,37.7749", + "brand": "Noka Bikes", + "model": "Kahuna", + "price": 3200, + "description": "Whether you want to try your hand at XC racing or are looking " + + "for a lively trail bike that's just as inspiring on the climbs as it is over rougher " + + "ground, the Wilder is one heck of a bike built specifically for short women. Both the " + + "frames and components have been tweaked to include a women’s saddle, different bars " + + "and unique colourway.", + "condition": "used", + }, + { + "pickup_zone": "POLYGON((-0.1778 51.5524, 0.0822 51.5524, 0.0822 51.4024, " + + "-0.1778 51.4024, -0.1778 51.5524))", + "store_location": "-0.1278,51.5074", + "brand": "Breakout", + "model": "XBN 2.1 Alloy", + "price": 810, + "description": "The XBN 2.1 Alloy is our entry-level road bike – but that’s " + + "not to say that it’s a basic machine. With an internal weld aluminium frame, a full " + + "carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which " + + "doesn’t break the bank and delivers craved performance.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((2.1767 48.9016, 2.5267 48.9016, 2.5267 48.5516, " + + "2.1767 48.5516, 2.1767 48.9016))", + "store_location": "2.3522,48.8566", + "brand": "ScramBikes", + "model": "WattBike", + "price": 2300, + "description": "The WattBike is the best e-bike for people who still " + + "feel young at heart. It has a Bafang 1000W mid-drive system and a 48V 17.5AH " + + "Samsung Lithium-Ion battery, allowing you to ride for more than 60 miles on one " + + "charge. It’s great for tackling hilly terrain or if you just fancy a more " + + "leisurely ride. With three working modes, you can choose between E-bike, " + + "assisted bicycle, and normal bike modes.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((13.3260 52.5700, 13.6550 52.5700, 13.6550 52.2700, " + + "13.3260 52.2700, 13.3260 52.5700))", + "store_location": "13.4050,52.5200", + "brand": "Peaknetic", + "model": "Secto", + "price": 430, + "description": "If you struggle with stiff fingers or a kinked neck or " + + "back after a few minutes on the road, this lightweight, aluminum bike alleviates " + + "those issues and allows you to enjoy the ride. From the ergonomic grips to the " + + "lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. " + + "The rear-inclined seat tube facilitates stability by allowing you to put a foot " + + "on the ground to balance at a stop, and the low step-over frame makes it " + + "accessible for all ability and mobility levels. The saddle is very soft, with " + + "a wide back to support your hip joints and a cutout in the center to redistribute " + + "that pressure. Rim brakes deliver satisfactory braking control, and the wide tires " + + "provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts " + + "facilitate setting up the Roll Low-Entry as your preferred commuter, and the " + + "BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((1.9450 41.4301, 2.4018 41.4301, 2.4018 41.1987, " + + "1.9450 41.1987, 1.9450 41.4301))", + "store_location": "2.1734, 41.3851", + "brand": "nHill", + "model": "Summit", + "price": 1200, + "description": "This budget mountain bike from nHill performs well both " + + "on bike paths and on the trail. The fork with 100mm of travel absorbs rough " + + "terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. " + + "The Shimano Tourney drivetrain offered enough gears for finding a comfortable " + + "pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. " + + "Whether you want an affordable bike that you can take to work, but also take " + + "trail in mountains on the weekends or you’re just after a stable, comfortable " + + "ride for the bike path, the Summit gives a good value for money.", + "condition": "new", + }, + { + "pickup_zone": "POLYGON((12.4464 42.1028, 12.5464 42.1028, " + + "12.5464 41.7028, 12.4464 41.7028, 12.4464 42.1028))", + "store_location": "12.4964,41.9028", + "model": "ThrillCycle", + "brand": "BikeShind", + "price": 815, + "description": "An artsy, retro-inspired bicycle that’s as " + + "functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. " + + "A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t " + + "suggest taking it to the mountains. Fenders protect you from mud, and a rear " + + "basket lets you transport groceries, flowers and books. The ThrillCycle comes " + + "with a limited lifetime warranty, so this little guy will last you long " + + "past graduation.", + "condition": "refurbished", + }, + } + + for i, json := range exampleJsons { + _, err := rdb.JSONSet(ctx, fmt.Sprintf("bicycle:%v", i), "$", json).Result() + + if err != nil { + panic(err) + } + } + + // STEP_START range1 + res1, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "@price:[500 1000]", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res1.Total) // >>> 3 + + for _, doc := range res1.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 + // STEP_END + + // STEP_START range2 + res2, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTSearchOptions{ + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: 500, + Max: 1000, + }, + }, + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res2.Total) // >>> 3 + + for _, doc := range res2.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:2 : price 815 + // >>> bicycle:5 : price 810 + // >>> bicycle:9 : price 815 + // STEP_END + + // STEP_START range3 + res3, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", "*", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + Filters: []redis.FTSearchFilter{ + { + FieldName: "price", + Min: "(1000", + Max: "+inf", + }, + }, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res3.Total) // >>> 5 + + for _, doc := range res3.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:1 : price 1200 + // >>> bicycle:4 : price 3200 + // >>> bicycle:6 : price 2300 + // >>> bicycle:3 : price 3400 + // >>> bicycle:8 : price 1200 + // STEP_END + + // STEP_START range4 + res4, err := rdb.FTSearchWithArgs(ctx, + "idx:bicycle", + "@price:[-inf 2000]", + &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{ + { + FieldName: "price", + }, + }, + SortBy: []redis.FTSearchSortBy{ + { + FieldName: "price", + Asc: true, + }, + }, + LimitOffset: 0, + Limit: 5, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(res4.Total) // >>> 7 + + for _, doc := range res4.Docs { + fmt.Printf("%v : price %v\n", doc.ID, doc.Fields["price"]) + } + // >>> bicycle:0 : price 270 + // >>> bicycle:7 : price 430 + // >>> bicycle:5 : price 810 + // >>> bicycle:2 : price 815 + // >>> bicycle:9 : price 815 + // STEP_END + + // Output: + // 3 + // bicycle:2 : price 815 + // bicycle:5 : price 810 + // bicycle:9 : price 815 + // 3 + // bicycle:2 : price 815 + // bicycle:5 : price 810 + // bicycle:9 : price 815 + // 5 + // bicycle:1 : price 1200 + // bicycle:4 : price 3200 + // bicycle:6 : price 2300 + // bicycle:3 : price 3400 + // bicycle:8 : price 1200 + // 7 + // bicycle:0 : price 270 + // bicycle:7 : price 430 + // bicycle:5 : price 810 + // bicycle:2 : price 815 + // bicycle:9 : price 815 +} From 37accb4b2807e1bec0d90079fd5e1506882cf6fe Mon Sep 17 00:00:00 2001 From: Ali Error Date: Thu, 20 Feb 2025 17:54:11 +0300 Subject: [PATCH 66/68] fix: nil pointer dereferencing in writeArg (#3271) * fixed bug with nil dereferencing in writeArg, added hset struct example, added tests * removed password from example * added omitempty * reverted xxhash versioning * reverted xxhash versioning * removed password * removed password --------- Co-authored-by: Nedyalko Dyakov --- example/hset-struct/README.md | 7 ++ example/hset-struct/go.mod | 15 ++++ example/hset-struct/go.sum | 10 +++ example/hset-struct/main.go | 129 ++++++++++++++++++++++++++++++++++ example/scan-struct/main.go | 6 ++ internal/proto/writer.go | 53 ++++++++++++++ internal/proto/writer_test.go | 83 ++++++++++++++-------- 7 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 example/hset-struct/README.md create mode 100644 example/hset-struct/go.mod create mode 100644 example/hset-struct/go.sum create mode 100644 example/hset-struct/main.go diff --git a/example/hset-struct/README.md b/example/hset-struct/README.md new file mode 100644 index 000000000..e6cb4523c --- /dev/null +++ b/example/hset-struct/README.md @@ -0,0 +1,7 @@ +# Example for setting struct fields as hash fields + +To run this example: + +```shell +go run . +``` diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod new file mode 100644 index 000000000..fca1a5972 --- /dev/null +++ b/example/hset-struct/go.mod @@ -0,0 +1,15 @@ +module github.com/redis/go-redis/example/scan-struct + +go 1.18 + +replace github.com/redis/go-redis/v9 => ../.. + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/redis/go-redis/v9 v9.6.2 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect +) diff --git a/example/hset-struct/go.sum b/example/hset-struct/go.sum new file mode 100644 index 000000000..1602e702e --- /dev/null +++ b/example/hset-struct/go.sum @@ -0,0 +1,10 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/example/hset-struct/main.go b/example/hset-struct/main.go new file mode 100644 index 000000000..2e08f542f --- /dev/null +++ b/example/hset-struct/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "time" + + "github.com/davecgh/go-spew/spew" + + "github.com/redis/go-redis/v9" +) + +type Model struct { + Str1 string `redis:"str1"` + Str2 string `redis:"str2"` + Str3 *string `redis:"str3"` + Str4 *string `redis:"str4"` + Bytes []byte `redis:"bytes"` + Int int `redis:"int"` + Int2 *int `redis:"int2"` + Int3 *int `redis:"int3"` + Bool bool `redis:"bool"` + Bool2 *bool `redis:"bool2"` + Bool3 *bool `redis:"bool3"` + Bool4 *bool `redis:"bool4,omitempty"` + Time time.Time `redis:"time"` + Time2 *time.Time `redis:"time2"` + Time3 *time.Time `redis:"time3"` + Ignored struct{} `redis:"-"` +} + +func main() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: ":6379", + }) + + _ = rdb.FlushDB(ctx).Err() + + t := time.Date(2025, 02, 8, 0, 0, 0, 0, time.UTC) + + data := Model{ + Str1: "hello", + Str2: "world", + Str3: ToPtr("hello"), + Str4: nil, + Bytes: []byte("this is bytes !"), + Int: 123, + Int2: ToPtr(0), + Int3: nil, + Bool: true, + Bool2: ToPtr(false), + Bool3: nil, + Time: t, + Time2: ToPtr(t), + Time3: nil, + Ignored: struct{}{}, + } + + // Set some fields. + if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { + rdb.HMSet(ctx, "key", data) + return nil + }); err != nil { + panic(err) + } + + var model1, model2 Model + + // Scan all fields into the model. + if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { + panic(err) + } + + // Or scan a subset of the fields. + if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { + panic(err) + } + + spew.Dump(model1) + // Output: + // (main.Model) { + // Str1: (string) (len=5) "hello", + // Str2: (string) (len=5) "world", + // Str3: (*string)(0xc000016970)((len=5) "hello"), + // Str4: (*string)(0xc000016980)(""), + // Bytes: ([]uint8) (len=15 cap=16) { + // 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !| + // }, + // Int: (int) 123, + // Int2: (*int)(0xc000014568)(0), + // Int3: (*int)(0xc000014560)(0), + // Bool: (bool) true, + // Bool2: (*bool)(0xc000014570)(false), + // Bool3: (*bool)(0xc000014548)(false), + // Bool4: (*bool)(), + // Time: (time.Time) 2025-02-08 00:00:00 +0000 UTC, + // Time2: (*time.Time)(0xc0000122a0)(2025-02-08 00:00:00 +0000 UTC), + // Time3: (*time.Time)(0xc000012288)(0001-01-01 00:00:00 +0000 UTC), + // Ignored: (struct {}) { + // } + // } + + spew.Dump(model2) + // Output: + // (main.Model) { + // Str1: (string) (len=5) "hello", + // Str2: (string) "", + // Str3: (*string)(), + // Str4: (*string)(), + // Bytes: ([]uint8) , + // Int: (int) 123, + // Int2: (*int)(), + // Int3: (*int)(), + // Bool: (bool) false, + // Bool2: (*bool)(), + // Bool3: (*bool)(), + // Bool4: (*bool)(), + // Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + // Time2: (*time.Time)(), + // Time3: (*time.Time)(), + // Ignored: (struct {}) { + // } + // } +} + +func ToPtr[T any](v T) *T { + return &v +} diff --git a/example/scan-struct/main.go b/example/scan-struct/main.go index cc877b847..2dc5b85c1 100644 --- a/example/scan-struct/main.go +++ b/example/scan-struct/main.go @@ -11,9 +11,12 @@ import ( type Model struct { Str1 string `redis:"str1"` Str2 string `redis:"str2"` + Str3 *string `redis:"str3"` Bytes []byte `redis:"bytes"` Int int `redis:"int"` + Int2 *int `redis:"int2"` Bool bool `redis:"bool"` + Bool2 *bool `redis:"bool2"` Ignored struct{} `redis:"-"` } @@ -29,8 +32,11 @@ func main() { if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { rdb.HSet(ctx, "key", "str1", "hello") rdb.HSet(ctx, "key", "str2", "world") + rdb.HSet(ctx, "key", "str3", "") rdb.HSet(ctx, "key", "int", 123) + rdb.HSet(ctx, "key", "int2", 0) rdb.HSet(ctx, "key", "bool", 1) + rdb.HSet(ctx, "key", "bool2", 0) rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !")) return nil }); err != nil { diff --git a/internal/proto/writer.go b/internal/proto/writer.go index 78595cc4f..38e66c688 100644 --- a/internal/proto/writer.go +++ b/internal/proto/writer.go @@ -66,56 +66,95 @@ func (w *Writer) WriteArg(v interface{}) error { case string: return w.string(v) case *string: + if v == nil { + return w.string("") + } return w.string(*v) case []byte: return w.bytes(v) case int: return w.int(int64(v)) case *int: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int8: return w.int(int64(v)) case *int8: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int16: return w.int(int64(v)) case *int16: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int32: return w.int(int64(v)) case *int32: + if v == nil { + return w.int(0) + } return w.int(int64(*v)) case int64: return w.int(v) case *int64: + if v == nil { + return w.int(0) + } return w.int(*v) case uint: return w.uint(uint64(v)) case *uint: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint8: return w.uint(uint64(v)) case *uint8: + if v == nil { + return w.string("") + } return w.uint(uint64(*v)) case uint16: return w.uint(uint64(v)) case *uint16: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint32: return w.uint(uint64(v)) case *uint32: + if v == nil { + return w.uint(0) + } return w.uint(uint64(*v)) case uint64: return w.uint(v) case *uint64: + if v == nil { + return w.uint(0) + } return w.uint(*v) case float32: return w.float(float64(v)) case *float32: + if v == nil { + return w.float(0) + } return w.float(float64(*v)) case float64: return w.float(v) case *float64: + if v == nil { + return w.float(0) + } return w.float(*v) case bool: if v { @@ -123,6 +162,9 @@ func (w *Writer) WriteArg(v interface{}) error { } return w.int(0) case *bool: + if v == nil { + return w.int(0) + } if *v { return w.int(1) } @@ -130,8 +172,19 @@ func (w *Writer) WriteArg(v interface{}) error { case time.Time: w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano) return w.bytes(w.numBuf) + case *time.Time: + if v == nil { + v = &time.Time{} + } + w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano) + return w.bytes(w.numBuf) case time.Duration: return w.int(v.Nanoseconds()) + case *time.Duration: + if v == nil { + return w.int(0) + } + return w.int(v.Nanoseconds()) case encoding.BinaryMarshaler: b, err := v.MarshalBinary() if err != nil { diff --git a/internal/proto/writer_test.go b/internal/proto/writer_test.go index 7c9d20884..1d5152dc0 100644 --- a/internal/proto/writer_test.go +++ b/internal/proto/writer_test.go @@ -111,36 +111,61 @@ var _ = Describe("WriteArg", func() { wr = proto.NewWriter(buf) }) + t := time.Date(2025, 2, 8, 00, 00, 00, 0, time.UTC) + args := map[any]string{ - "hello": "$5\r\nhello\r\n", - int(10): "$2\r\n10\r\n", - util.ToPtr(int(10)): "$2\r\n10\r\n", - int8(10): "$2\r\n10\r\n", - util.ToPtr(int8(10)): "$2\r\n10\r\n", - int16(10): "$2\r\n10\r\n", - util.ToPtr(int16(10)): "$2\r\n10\r\n", - int32(10): "$2\r\n10\r\n", - util.ToPtr(int32(10)): "$2\r\n10\r\n", - int64(10): "$2\r\n10\r\n", - util.ToPtr(int64(10)): "$2\r\n10\r\n", - uint(10): "$2\r\n10\r\n", - util.ToPtr(uint(10)): "$2\r\n10\r\n", - uint8(10): "$2\r\n10\r\n", - util.ToPtr(uint8(10)): "$2\r\n10\r\n", - uint16(10): "$2\r\n10\r\n", - util.ToPtr(uint16(10)): "$2\r\n10\r\n", - uint32(10): "$2\r\n10\r\n", - util.ToPtr(uint32(10)): "$2\r\n10\r\n", - uint64(10): "$2\r\n10\r\n", - util.ToPtr(uint64(10)): "$2\r\n10\r\n", - float32(10.3): "$18\r\n10.300000190734863\r\n", - util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n", - float64(10.3): "$4\r\n10.3\r\n", - util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n", - bool(true): "$1\r\n1\r\n", - bool(false): "$1\r\n0\r\n", - util.ToPtr(bool(true)): "$1\r\n1\r\n", - util.ToPtr(bool(false)): "$1\r\n0\r\n", + "hello": "$5\r\nhello\r\n", + util.ToPtr("hello"): "$5\r\nhello\r\n", + (*string)(nil): "$0\r\n\r\n", + int(10): "$2\r\n10\r\n", + util.ToPtr(int(10)): "$2\r\n10\r\n", + (*int)(nil): "$1\r\n0\r\n", + int8(10): "$2\r\n10\r\n", + util.ToPtr(int8(10)): "$2\r\n10\r\n", + (*int8)(nil): "$1\r\n0\r\n", + int16(10): "$2\r\n10\r\n", + util.ToPtr(int16(10)): "$2\r\n10\r\n", + (*int16)(nil): "$1\r\n0\r\n", + int32(10): "$2\r\n10\r\n", + util.ToPtr(int32(10)): "$2\r\n10\r\n", + (*int32)(nil): "$1\r\n0\r\n", + int64(10): "$2\r\n10\r\n", + util.ToPtr(int64(10)): "$2\r\n10\r\n", + (*int64)(nil): "$1\r\n0\r\n", + uint(10): "$2\r\n10\r\n", + util.ToPtr(uint(10)): "$2\r\n10\r\n", + (*uint)(nil): "$1\r\n0\r\n", + uint8(10): "$2\r\n10\r\n", + util.ToPtr(uint8(10)): "$2\r\n10\r\n", + (*uint8)(nil): "$0\r\n\r\n", + uint16(10): "$2\r\n10\r\n", + util.ToPtr(uint16(10)): "$2\r\n10\r\n", + (*uint16)(nil): "$1\r\n0\r\n", + uint32(10): "$2\r\n10\r\n", + util.ToPtr(uint32(10)): "$2\r\n10\r\n", + (*uint32)(nil): "$1\r\n0\r\n", + uint64(10): "$2\r\n10\r\n", + util.ToPtr(uint64(10)): "$2\r\n10\r\n", + (*uint64)(nil): "$1\r\n0\r\n", + float32(10.3): "$18\r\n10.300000190734863\r\n", + util.ToPtr(float32(10.3)): "$18\r\n10.300000190734863\r\n", + (*float32)(nil): "$1\r\n0\r\n", + float64(10.3): "$4\r\n10.3\r\n", + util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n", + (*float64)(nil): "$1\r\n0\r\n", + bool(true): "$1\r\n1\r\n", + bool(false): "$1\r\n0\r\n", + util.ToPtr(bool(true)): "$1\r\n1\r\n", + util.ToPtr(bool(false)): "$1\r\n0\r\n", + (*bool)(nil): "$1\r\n0\r\n", + time.Time(t): "$20\r\n2025-02-08T00:00:00Z\r\n", + util.ToPtr(time.Time(t)): "$20\r\n2025-02-08T00:00:00Z\r\n", + (*time.Time)(nil): "$20\r\n0001-01-01T00:00:00Z\r\n", + time.Duration(time.Second): "$10\r\n1000000000\r\n", + util.ToPtr(time.Duration(time.Second)): "$10\r\n1000000000\r\n", + (*time.Duration)(nil): "$1\r\n0\r\n", + (encoding.BinaryMarshaler)(&MyType{}): "$5\r\nhello\r\n", + (encoding.BinaryMarshaler)(nil): "$0\r\n\r\n", } for arg, expect := range args { From 30e7388c88e29e11def4302ae740b677a6309019 Mon Sep 17 00:00:00 2001 From: "fengyun.rui" Date: Thu, 20 Feb 2025 22:55:54 +0800 Subject: [PATCH 67/68] fix: race slice for list function of ring client (#2931) * fix: race slice for list of ring client Signed-off-by: rfyiamcool * fix: copy wrong list Co-authored-by: Nedyalko Dyakov --------- Signed-off-by: rfyiamcool Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Co-authored-by: Nedyalko Dyakov --- ring.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ring.go b/ring.go index b40221734..06a26020a 100644 --- a/ring.go +++ b/ring.go @@ -341,7 +341,8 @@ func (c *ringSharding) List() []*ringShard { c.mu.RLock() if !c.closed { - list = c.shards.list + list = make([]*ringShard, len(c.shards.list)) + copy(list, c.shards.list) } c.mu.RUnlock() From aa7019d718476abf3da28410869b828c337f46bc Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 24 Feb 2025 12:45:08 +0200 Subject: [PATCH 68/68] V9.7.1 -> master (#3287) --- .github/workflows/build.yml | 4 ++-- .github/workflows/golangci-lint.yml | 6 ++---- example/del-keys-without-ttl/go.mod | 2 +- example/hll/go.mod | 2 +- example/lua-scripting/go.mod | 2 +- example/otel/go.mod | 6 +++--- example/redis-bloom/go.mod | 2 +- example/scan-struct/go.mod | 2 +- extra/rediscensus/go.mod | 4 ++-- extra/rediscmd/go.mod | 2 +- extra/redisotel/go.mod | 4 ++-- extra/redisprometheus/go.mod | 2 +- search_test.go | 1 - version.go | 2 +- 14 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5852fcde4..afec49d8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9] + branches: [master, v9, v9.7] pull_request: - branches: [master, v9] + branches: [master, v9, v9.7] permissions: contents: read diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5210ccfa2..d9e53f706 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -12,15 +12,13 @@ on: permissions: contents: read + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests jobs: golangci: - permissions: - contents: read # for actions/checkout to fetch code - pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v6.5.0 diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 9290eb039..40ad6297f 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index e2bf03d92..14a8827fd 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 5c811bf25..64f5c8af1 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index f5e2a156c..93b5d46c2 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -9,8 +9,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +23,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 9076e1474..a973cd179 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.6.2 +require github.com/redis/go-redis/v9 v9.7.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index f14f54df1..21d7e527d 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index a28ad7dfd..cc0bd0fb5 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 go.opencensus.io v0.24.0 ) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 07df0cc2d..0689fe904 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 47aab0db1..ab6288dec 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.6.2 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.1 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 42a6f805c..a1659bb0f 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.1 ) require ( diff --git a/search_test.go b/search_test.go index ea3460d3d..e08ce3d39 100644 --- a/search_test.go +++ b/search_test.go @@ -667,7 +667,6 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) It("should FTAggregate with scorer and addscores", Label("search", "ftaggregate", "NonRedisEnterprise"), func() { - SkipBeforeRedisMajor(8, "ADDSCORES is available in Redis CE 8") title := &redis.FieldSchema{FieldName: "title", FieldType: redis.SearchFieldTypeText, Sortable: false} description := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText, Sortable: false} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{OnHash: true, Prefix: []interface{}{"product:"}}, title, description).Result() diff --git a/version.go b/version.go index 7cb060b5d..a447a546d 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.6.2" + return "9.7.1" }