Use iter.Seq to iterate over ScanIterator (#3348)

* Add support for iter.Seq for ScanIterator

* Simplify Vals

* Review fixes
This commit is contained in:
Bulat Khasanov authored and GitHub committed 2026-10-20 09:48:08 +03:00
1 parent 3f37a3a4e4
commit ad6e893f5f
4 files changed
+146 -14

No files matched your search

+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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)
+51
View File
@@ -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 {
@@ -65,6 +68,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) {
@@ -72,3 +77,49 @@ 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
}
// Await the fetch before reading page/cursor: on the deferred
// autopipeline face process() only enqueues, and reading the raw
// fields of a not-yet-executed command would spin re-issuing SCANs
// with a stale cursor forever. Err() blocks until executed there and
// is a no-op read everywhere else.
if it.cmd.Err() != nil {
return
}
}
}
}
+89 -8
View File
@@ -2,6 +2,7 @@ package redis_test
import (
"fmt"
"slices"
. "github.com/bsm/ginkgo/v2"
. "github.com/bsm/gomega"
@@ -55,13 +56,20 @@ var _ = Describe("ScanIterator", func() {
Expect(closeSubject()).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
@@ -73,7 +81,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
@@ -87,7 +104,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())
@@ -103,7 +131,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())
@@ -119,7 +160,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
@@ -131,7 +185,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
@@ -143,7 +206,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
@@ -154,4 +226,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))
})
})