diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index bba99197..75b12827 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-M01-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ["7.2.x"]="rs-7.2.0-v17" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd097a9f..8424f63c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] pull_request: - branches: [master, v9, v9.7, v9.8] + branches: [master, v9, v9.7, v9.8, 'ndyakov/*', 'ofekshenawa/*', 'htemelski-redis/*', 'ce/*'] permissions: contents: read @@ -44,7 +44,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.2.x"]="8.2-M01-pre" + ["8.2.x"]="8.2-rc2-pre" ["8.0.x"]="8.0.2" ["7.4.x"]="rs-7.4.0-v5" ) diff --git a/commands_test.go b/commands_test.go index 19548e13..9e130089 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6169,6 +6169,34 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XTrimMaxLenMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenMode(ctx, "stream", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMaxLenApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMaxLenApproxMode(ctx, "stream", 0, 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDMode(ctx, "stream", "4-0", "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + + It("should XTrimMinIDApproxMode", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + n, err := client.XTrimMinIDApproxMode(ctx, "stream", "4-0", 0, "KEEPREF").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(BeNumerically(">=", 0)) + }) + It("should XAdd", func() { id, err := client.XAdd(ctx, &redis.XAddArgs{ Stream: "stream", @@ -6222,6 +6250,37 @@ var _ = Describe("Commands", func() { Expect(n).To(Equal(int64(3))) }) + It("should XAckDel", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // First, create a consumer group + err := client.XGroupCreate(ctx, "stream", "testgroup", "0").Err() + Expect(err).NotTo(HaveOccurred()) + + // Read messages to create pending entries + _, err = client.XReadGroup(ctx, &redis.XReadGroupArgs{ + Group: "testgroup", + Consumer: "testconsumer", + Streams: []string{"stream", ">"}, + }).Result() + Expect(err).NotTo(HaveOccurred()) + + // Test XAckDel with KEEPREF mode + n, err := client.XAckDel(ctx, "stream", "testgroup", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + + // Clean up + client.XGroupDestroy(ctx, "stream", "testgroup") + }) + + It("should XDelEx", func() { + SkipBeforeRedisVersion(8.2, "doesn't work with older redis stack images") + // Test XDelEx with KEEPREF mode + n, err := client.XDelEx(ctx, "stream", "KEEPREF", "1-0", "2-0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(HaveLen(2)) + }) + It("should XLen", func() { n, err := client.XLen(ctx, "stream").Result() Expect(err).NotTo(HaveOccurred()) diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go index c497b722..2e944274 100644 --- a/doctests/geo_index_test.go +++ b/doctests/geo_index_test.go @@ -199,11 +199,11 @@ func ExampleClient_geoindex() { // OK // OK // OK - // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}] }]} // OK // OK // OK // OK // OK - // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]] }]} } diff --git a/doctests/home_json_example_test.go b/doctests/home_json_example_test.go index f32bf8d1..d2af4d73 100644 --- a/doctests/home_json_example_test.go +++ b/doctests/home_json_example_test.go @@ -219,7 +219,7 @@ func ExampleClient_search_json() { // STEP_END // Output: - // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}]}]} + // {1 [{user:3 map[$:{"age":35,"city":"Tel Aviv","email":"paul.zamir@example.com","name":"Paul Zamir"}] }]} // London // Tel Aviv // 0 @@ -329,5 +329,5 @@ func ExampleClient_search_hash() { // STEP_END // Output: - // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir]}]} + // {1 [{huser:3 map[age:35 city:Tel Aviv email:paul.zamir@example.com name:Paul Zamir] }]} } diff --git a/doctests/search_quickstart_test.go b/doctests/search_quickstart_test.go index ce5033bc..a41c9c5c 100644 --- a/doctests/search_quickstart_test.go +++ b/doctests/search_quickstart_test.go @@ -257,6 +257,6 @@ func ExampleClient_search_qs() { // Output: // Documents found: 10 - // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}]}]} - // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}]}]} + // {1 [{bicycle:0 map[$:{"brand":"Velorim","condition":"new","description":"Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go.","model":"Jigger","price":270}] }]} + // {1 [{bicycle:4 map[$:{"brand":"Noka Bikes","condition":"used","description":"Whether you want to try your hand at XC racing or are looking for a lively trail bike that's just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.","model":"Kahuna","price":3200}] }]} } diff --git a/search_commands.go b/search_commands.go index a5332480..f0ca1bfe 100644 --- a/search_commands.go +++ b/search_commands.go @@ -488,6 +488,7 @@ type Document struct { Payload *string SortKey *string Fields map[string]string + Error error } type AggregateQuery []interface{} @@ -1735,7 +1736,13 @@ func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, with if i < len(data) { fields, ok := data[i].([]interface{}) if !ok { - return FTSearchResult{}, fmt.Errorf("invalid document fields format") + if data[i] == proto.Nil || data[i] == nil { + doc.Error = proto.Nil + doc.Fields = map[string]string{} + fields = []interface{}{} + } else { + return FTSearchResult{}, fmt.Errorf("invalid document fields format") + } } for j := 0; j < len(fields); j += 2 { diff --git a/stream_commands.go b/stream_commands.go index 6d7b2292..4b84e00f 100644 --- a/stream_commands.go +++ b/stream_commands.go @@ -7,7 +7,9 @@ import ( type StreamCmdable interface { XAdd(ctx context.Context, a *XAddArgs) *StringCmd + XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd XDel(ctx context.Context, stream string, ids ...string) *IntCmd + XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd XLen(ctx context.Context, stream string) *IntCmd XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd @@ -31,8 +33,12 @@ type StreamCmdable interface { XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd + XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd + XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd XTrimMinID(ctx context.Context, key string, minID string) *IntCmd XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd + XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd + XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd XInfoStream(ctx context.Context, key string) *XInfoStreamCmd XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd @@ -54,6 +60,7 @@ type XAddArgs struct { // Approx causes MaxLen and MinID to use "~" matcher (instead of "="). Approx bool Limit int64 + Mode string ID string Values interface{} } @@ -81,6 +88,11 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { if a.Limit > 0 { args = append(args, "limit", a.Limit) } + + if a.Mode != "" { + args = append(args, a.Mode) + } + if a.ID != "" { args = append(args, a.ID) } else { @@ -93,6 +105,16 @@ func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd { return cmd } +func (c cmdable) XAckDel(ctx context.Context, stream string, group string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xackdel", stream, group, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd { args := []interface{}{"xdel", stream} for _, id := range ids { @@ -103,6 +125,16 @@ func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd return cmd } +func (c cmdable) XDelEx(ctx context.Context, stream string, mode string, ids ...string) *SliceCmd { + args := []interface{}{"xdelex", stream, mode, "ids", len(ids)} + for _, id := range ids { + args = append(args, id) + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd { cmd := NewIntCmd(ctx, "xlen", stream) _ = c(ctx, cmd) @@ -375,6 +407,8 @@ func xClaimArgs(a *XClaimArgs) []interface{} { return args } +// TODO: refactor xTrim, xTrimMode and the wrappers over the functions + // xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default). // example: // @@ -418,6 +452,42 @@ func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, return c.xTrim(ctx, key, "minid", true, minID, limit) } +func (c cmdable) xTrimMode( + ctx context.Context, key, strategy string, + approx bool, threshold interface{}, limit int64, + mode string, +) *IntCmd { + args := make([]interface{}, 0, 7) + args = append(args, "xtrim", key, strategy) + if approx { + args = append(args, "~") + } + args = append(args, threshold) + if limit > 0 { + args = append(args, "limit", limit) + } + args = append(args, mode) + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", false, maxLen, 0, mode) +} + +func (c cmdable) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "maxlen", true, maxLen, limit, mode) +} + +func (c cmdable) XTrimMinIDMode(ctx context.Context, key string, minID string, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", false, minID, 0, mode) +} + +func (c cmdable) XTrimMinIDApproxMode(ctx context.Context, key string, minID string, limit int64, mode string) *IntCmd { + return c.xTrimMode(ctx, key, "minid", true, minID, limit, mode) +} + func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd { cmd := NewXInfoConsumersCmd(ctx, key, group) _ = c(ctx, cmd) diff --git a/vectorset_commands.go b/vectorset_commands.go index 2bd9e221..96be1af1 100644 --- a/vectorset_commands.go +++ b/vectorset_commands.go @@ -287,8 +287,7 @@ type VSimArgs struct { FilterEF int64 Truth bool NoThread bool - // The `VSim` command in Redis has the option, by the doc in Redis.io don't have. - // Epsilon float64 + Epsilon float64 } func (v VSimArgs) appendArgs(args []any) []any { @@ -310,13 +309,13 @@ func (v VSimArgs) appendArgs(args []any) []any { if v.NoThread { args = append(args, "nothread") } - // if v.Epsilon > 0 { - // args = append(args, "Epsilon", v.Epsilon) - // } + if v.Epsilon > 0 { + args = append(args, "Epsilon", v.Epsilon) + } return args } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *StringSliceCmd { @@ -331,7 +330,7 @@ func (c cmdable) VSimWithArgs(ctx context.Context, key string, val Vector, simAr return cmd } -// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [COUNT num] [EPSILON delta] // [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` // note: the API is experimental and may be subject to change. func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreSliceCmd {