diff --git a/README.md b/README.md index 6c42c0fc2..d69e6e524 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ In `go-redis` we are aiming to support the last three releases of Redis. Current - [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support - [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included -Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three +Although the `go.mod` states it requires at minimum `go 1.23`, our CI is configured to run the tests against all three versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0), [1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with Redis Stack 7.2 and some commands are changed with Redis CE 8.0. diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 727fbbd7f..c3af4fcc5 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/del-keys-without-ttl -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/del-keys-without-ttl/main.go b/example/del-keys-without-ttl/main.go index d2f85d4e8..560c476c0 100644 --- a/example/del-keys-without-ttl/main.go +++ b/example/del-keys-without-ttl/main.go @@ -28,8 +28,8 @@ func main() { checker.Start(ctx) iter := rdb.Scan(ctx, 0, "", 0).Iterator() - for iter.Next(ctx) { - checker.Add(iter.Val()) + for val := range iter.Vals(ctx) { + checker.Add(val) } if err := iter.Err(); err != nil { panic(err) diff --git a/example/hll/go.mod b/example/hll/go.mod index 775e3e7b1..b338586e4 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/hll -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index 33d3ef6d8..dad6aa654 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/scan-struct -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 363c93c29..c7d694910 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/lua-scripting -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index 3d7a4caaa..c9acb592a 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/redis-bloom -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 33d3ef6d8..dad6aa654 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/scan-struct -go 1.18 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example_test.go b/example_test.go index 28d14b65a..252216f88 100644 --- a/example_test.go +++ b/example_test.go @@ -632,8 +632,8 @@ func Example_customCommand2() { func ExampleScanIterator() { iter := rdb.Scan(ctx, 0, "", 0).Iterator() - for iter.Next(ctx) { - fmt.Println(iter.Val()) + for val := range iter.Vals(ctx) { + fmt.Println(val) } if err := iter.Err(); err != nil { panic(err) @@ -642,8 +642,8 @@ func ExampleScanIterator() { func ExampleScanCmd_Iterator() { iter := rdb.Scan(ctx, 0, "", 0).Iterator() - for iter.Next(ctx) { - fmt.Println(iter.Val()) + for val := range iter.Vals(ctx) { + fmt.Println(val) } if err := iter.Err(); err != nil { panic(err) diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 7033e805f..61a882362 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/rediscensus/v9 -go 1.19 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index c1cff3e90..25b16edbf 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/rediscmd/v9 -go 1.19 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index e5b442e61..b5eb2d64d 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/redisotel/v9 -go 1.19 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index 8bff00086..27bf3994e 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/redisprometheus/v9 -go 1.19 +go 1.23 replace github.com/redis/go-redis/v9 => ../.. diff --git a/go.mod b/go.mod index 83e8fd3d6..5d6517a69 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/v9 -go 1.18 +go 1.23 require ( github.com/bsm/ginkgo/v2 v2.12.0 diff --git a/internal/customvet/go.mod b/internal/customvet/go.mod index edaa2d7ba..571dddb49 100644 --- a/internal/customvet/go.mod +++ b/internal/customvet/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/internal/customvet -go 1.17 +go 1.23 require golang.org/x/tools v0.5.0 diff --git a/iterator.go b/iterator.go index cd1a82851..d0ba7fd87 100644 --- a/iterator.go +++ b/iterator.go @@ -2,6 +2,7 @@ package redis import ( "context" + "iter" ) // ScanIterator is used to incrementally iterate over a collection of elements. @@ -16,6 +17,8 @@ func (it *ScanIterator) Err() error { } // Next advances the cursor and returns true if more values can be read. +// +// Deprecated: support for native iterators has been added in go 1.23. Use Vals instead. func (it *ScanIterator) Next(ctx context.Context) bool { // Instantly return on errors. if it.cmd.Err() != nil { @@ -57,6 +60,8 @@ func (it *ScanIterator) Next(ctx context.Context) bool { } // Val returns the key/field at the current cursor position. +// +// Deprecated: support for native iterators has been added in go 1.23. Use Vals instead. func (it *ScanIterator) Val() string { var v string if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) { @@ -64,3 +69,41 @@ func (it *ScanIterator) Val() string { } return v } + +// Vals returns iterator over key/field at the current cursor position. +func (it *ScanIterator) Vals(ctx context.Context) iter.Seq[string] { + return func(yield func(string) bool) { + if it.cmd.Err() != nil { + return + } + + for { + for _, val := range it.cmd.page { + if !yield(val) { + return + } + } + + // Return if there is no more data to fetch. + if it.cmd.cursor == 0 { + return + } + + // Fetch next page. + var cursorIndex int + switch it.cmd.args[0] { + case "scan", "qscan": + cursorIndex = 1 + default: + cursorIndex = 2 + } + + it.cmd.args[cursorIndex] = it.cmd.cursor + + err := it.cmd.process(ctx, it.cmd) + if err != nil { + return + } + } + } +} diff --git a/iterator_test.go b/iterator_test.go index c4f046476..77ea27e7e 100644 --- a/iterator_test.go +++ b/iterator_test.go @@ -2,6 +2,7 @@ package redis_test import ( "fmt" + "slices" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" @@ -52,13 +53,20 @@ var _ = Describe("ScanIterator", func() { Expect(client.Close()).NotTo(HaveOccurred()) }) - It("should scan across empty DBs", func() { + It("should scan across empty DBs using Next", func() { iter := client.Scan(ctx, 0, "", 10).Iterator() Expect(iter.Next(ctx)).To(BeFalse()) Expect(iter.Err()).NotTo(HaveOccurred()) }) - It("should scan across one page", func() { + It("should scan across empty DBs using Vals", func() { + iter := client.Scan(ctx, 0, "", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(vals).To(BeEmpty()) + Expect(iter.Err()).NotTo(HaveOccurred()) + }) + + It("should scan across one page using Next", func() { Expect(seed(7)).NotTo(HaveOccurred()) var vals []string @@ -70,7 +78,16 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(ConsistOf([]string{"K01", "K02", "K03", "K04", "K05", "K06", "K07"})) }) - It("should scan across multiple pages", func() { + It("should scan across one page using Vals", func() { + Expect(seed(7)).NotTo(HaveOccurred()) + + iter := client.Scan(ctx, 0, "", 0).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(ConsistOf([]string{"K01", "K02", "K03", "K04", "K05", "K06", "K07"})) + }) + + It("should scan across multiple pages using Next", func() { Expect(seed(71)).NotTo(HaveOccurred()) var vals []string @@ -84,7 +101,18 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(ContainElement("K71")) }) - It("should hscan across multiple pages", func() { + It("should scan across multiple pages using Vals", func() { + Expect(seed(71)).NotTo(HaveOccurred()) + + iter := client.Scan(ctx, 0, "", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(71)) + Expect(vals).To(ContainElement("K01")) + Expect(vals).To(ContainElement("K71")) + }) + + It("should hscan across multiple pages using Next", func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") Expect(hashSeed(71)).NotTo(HaveOccurred()) @@ -100,7 +128,20 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(ContainElement("x")) }) - It("should hscan without values across multiple pages", Label("NonRedisEnterprise"), func() { + It("should hscan across multiple pages using Vals", func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") + Expect(hashSeed(71)).NotTo(HaveOccurred()) + + iter := client.HScan(ctx, hashKey, 0, "", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(71 * 2)) + Expect(vals).To(ContainElement("K01")) + Expect(vals).To(ContainElement("K71")) + Expect(vals).To(ContainElement("x")) + }) + + It("should hscan without values across multiple pages using Next", Label("NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") Expect(hashSeed(71)).NotTo(HaveOccurred()) @@ -116,7 +157,20 @@ var _ = Describe("ScanIterator", func() { Expect(vals).NotTo(ContainElement("x")) }) - It("should scan to page borders", func() { + It("should hscan without values across multiple pages using Vals", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") + Expect(hashSeed(71)).NotTo(HaveOccurred()) + + iter := client.HScanNoValues(ctx, hashKey, 0, "", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(71)) + Expect(vals).To(ContainElement("K01")) + Expect(vals).To(ContainElement("K71")) + Expect(vals).NotTo(ContainElement("x")) + }) + + It("should scan to page borders using Next", func() { Expect(seed(20)).NotTo(HaveOccurred()) var vals []string @@ -128,7 +182,16 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(HaveLen(20)) }) - It("should scan with match", func() { + It("should scan to page borders using Vals", func() { + Expect(seed(20)).NotTo(HaveOccurred()) + + iter := client.Scan(ctx, 0, "", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(20)) + }) + + It("should scan with match using Next", func() { Expect(seed(33)).NotTo(HaveOccurred()) var vals []string @@ -140,7 +203,16 @@ var _ = Describe("ScanIterator", func() { Expect(vals).To(HaveLen(13)) }) - It("should scan with match across empty pages", func() { + It("should scan with match using Vals", func() { + Expect(seed(33)).NotTo(HaveOccurred()) + + iter := client.Scan(ctx, 0, "K*2*", 10).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(13)) + }) + + It("should scan with match across empty pages using Next", func() { Expect(extraSeed(2, 10)).NotTo(HaveOccurred()) var vals []string @@ -151,4 +223,13 @@ var _ = Describe("ScanIterator", func() { Expect(iter.Err()).NotTo(HaveOccurred()) Expect(vals).To(HaveLen(2)) }) + + It("should scan with match across empty pages using Vals", func() { + Expect(extraSeed(2, 10)).NotTo(HaveOccurred()) + + iter := client.Scan(ctx, 0, "K*", 1).Iterator() + vals := slices.Collect(iter.Vals(ctx)) + Expect(iter.Err()).NotTo(HaveOccurred()) + Expect(vals).To(HaveLen(2)) + }) })