Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: Implement Scan #3378

Merged
merged 19 commits into from
Mar 28, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
* Go: Add `FLUSHALL` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
* Go: Add `FLUSHDB` ([#3117](https://github.com/valkey-io/valkey-glide/pull/3117))
* Go: Add password update api ([#3346](https://github.com/valkey-io/valkey-glide/pull/3346))
* Go: Add `Scan` ([#3378](https://github.com/valkey-io/valkey-glide/pull/3378))

#### Breaking Changes

9 changes: 9 additions & 0 deletions go/api/generic_commands.go
Original file line number Diff line number Diff line change
@@ -2,11 +2,20 @@

package api

import (
"github.com/valkey-io/valkey-glide/go/api/options"
)

// GenericCommands supports commands for the "Generic Commands" group for standalone client.
//
// See [valkey.io] for details.
//
// [valkey.io]: https://valkey.io/commands/#generic
type GenericCommands interface {
CustomCommand(args []string) (interface{}, error)

Scan(cursor int64) (string, []string, error)

ScanWithOptions(cursor int64, scanOptions options.ScanOptions) (string, []string,
error)
}
35 changes: 35 additions & 0 deletions go/api/generic_commands_test.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ package api

import (
"fmt"

"github.com/valkey-io/valkey-glide/go/api/options"
)

func ExampleGlideClient_CustomCommand() {
@@ -16,3 +18,36 @@ func ExampleGlideClient_CustomCommand() {

// Output: PONG
}

func ExampleGlideClient_Scan() {
var client *GlideClient = getExampleGlideClient() // example helper function
client.CustomCommand([]string{"FLUSHALL"})
client.Set("key1", "hello")
resCursor, resCollection, err := client.Scan(0)
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println("Cursor:", resCursor)
fmt.Println("Collection:", resCollection)

// Output:
// Cursor: 0
// Collection: [key1]
}

func ExampleGlideClient_ScanWithOptions() {
var client *GlideClient = getExampleGlideClient() // example helper function
opts := options.NewScanOptions().SetCount(10).SetType(options.ObjectTypeList)
client.CustomCommand([]string{"FLUSHALL"})
client.LPush("key1", []string{"1", "3", "2", "4"})
resCursor, resCollection, err := client.ScanWithOptions(0, *opts)
if err != nil {
fmt.Println("Glide example failed with an error: ", err)
}
fmt.Println("Cursor:", resCursor)
fmt.Println("Collection:", resCollection)

// Output:
// Cursor: 0
// Collection: [key1]
}
49 changes: 49 additions & 0 deletions go/api/glide_client.go
Original file line number Diff line number Diff line change
@@ -314,3 +314,52 @@ func (client *GlideClient) FlushDBWithOptions(mode options.FlushMode) (string, e
}
return handleStringResponse(result)
}

// Iterates incrementally over a database for matching keys.
//
// Parameters:
//
// cursor - The cursor that points to the next iteration of results. A value of 0
// indicates the start of the search.
//
// Return value:
//
// An Array of Objects. The first element is always the cursor for the next
// iteration of results. "0" will be the cursor returned on the last iteration
// of the scan. The second element is always an Array of matched keys from the database.
//
// [valkey.io]: https://valkey.io/commands/scan/
func (client *GlideClient) Scan(cursor int64) (string, []string, error) {
res, err := client.executeCommand(C.Scan, []string{utils.IntToString(cursor)})
if err != nil {
return DefaultStringResponse, nil, err
}
return handleScanResponse(res)
}

// Iterates incrementally over a database for matching keys.
//
// Parameters:
//
// cursor - The cursor that points to the next iteration of results. A value of 0
// indicates the start of the search.
// scanOptions - Additional command parameters, see [ScanOptions] for more details.
//
// Return value:
//
// An Array of Objects. The first element is always the cursor for the next
// iteration of results. "0" will be the cursor returned on the last iteration
// of the scan. The second element is always an Array of matched keys from the database.
//
// [valkey.io]: https://valkey.io/commands/scan/
func (client *GlideClient) ScanWithOptions(cursor int64, scanOptions options.ScanOptions) (string, []string, error) {
optionArgs, err := scanOptions.ToArgs()
if err != nil {
return DefaultStringResponse, nil, err
}
res, err := client.executeCommand(C.Scan, append([]string{utils.IntToString(cursor)}, optionArgs...))
if err != nil {
return DefaultStringResponse, nil, err
}
return handleScanResponse(res)
}
46 changes: 46 additions & 0 deletions go/api/options/scan_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0

package options

type ScanOptions struct {
BaseScanOptions
Type ObjectType
}

func NewScanOptions() *ScanOptions {
return &ScanOptions{}
}

// SetMatch sets the match pattern for the SCAN command.
// It is possible to only iterate elements matching a given glob-style pattern,
// similarly to the behavior of the KEYS command that takes a pattern as its only argument.
func (scanOptions *ScanOptions) SetMatch(match string) *ScanOptions {
scanOptions.BaseScanOptions.SetMatch(match)
return scanOptions
}

// SetCount sets the count of the SCAN command.
// Basically with COUNT the user specifies the amount of work that
// should be done at every call in order to retrieve elements from the collection.
func (scanOptions *ScanOptions) SetCount(count int64) *ScanOptions {
scanOptions.BaseScanOptions.SetCount(count)
return scanOptions
}

// Set TYPE(string, list, set, zset, hash and stream)sets the type of the SCAN command.
// You can use the Type option to ask SCAN to only return objects that match a given type,
// allowing you to iterate through the database looking for keys of a specific type.
func (scanOptions *ScanOptions) SetType(typeOpts ObjectType) *ScanOptions {
scanOptions.Type = typeOpts
return scanOptions
}

func (opts *ScanOptions) ToArgs() ([]string, error) {
args := []string{}
baseArgs, err := opts.BaseScanOptions.ToArgs()
if opts.Type != "" {
args = append(args, TypeKeyword, string(opts.Type))
}
args = append(args, baseArgs...)
return args, err
}
32 changes: 32 additions & 0 deletions go/integTest/standalone_commands_test.go
Original file line number Diff line number Diff line change
@@ -789,3 +789,35 @@ func (suite *GlideTestSuite) TestUpdateConnectionPassword_ImmediateAuthWrongPass
_, err = adminClient.ConfigSet(map[string]string{"requirepass": ""})
assert.NoError(suite.T(), err)
}

func (suite *GlideTestSuite) TestScan() {
client := suite.defaultClient()
t := suite.T()
key := uuid.New().String()
suite.verifyOK(client.Set(key, "Hello"))
resCursor, resCollection, err := client.Scan(0)
assert.Nil(t, err)
assert.GreaterOrEqual(t, len(resCursor), 1)
assert.GreaterOrEqual(t, len(resCollection), 1)
}

func (suite *GlideTestSuite) TestScanWithOption() {
client := suite.defaultClient()
t := suite.T()

// Test TestScanWithOption SetCount
key := uuid.New().String()
suite.verifyOK(client.Set(key, "Hello"))
opts := options.NewScanOptions().SetCount(10)
resCursor, resCollection, err := client.ScanWithOptions(0, *opts)
assert.Nil(t, err)
assert.GreaterOrEqual(t, len(resCursor), 1)
assert.GreaterOrEqual(t, len(resCollection), 1)

// Test TestScanWithOption SetType
opts = options.NewScanOptions().SetType(options.ObjectTypeString)
resCursor, resCollection, err = client.ScanWithOptions(0, *opts)
assert.Nil(t, err)
assert.GreaterOrEqual(t, len(resCursor), 1)
assert.GreaterOrEqual(t, len(resCollection), 1)
}
Loading