diff --git a/go/api/base_client.go b/go/api/base_client.go index 7ceda57f16..eee59e3426 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -484,27 +484,83 @@ func (client *baseClient) HIncrByFloat(key string, field string, increment float return handleFloatResponse(result) } -func (client *baseClient) HScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the +// optional arguments of the command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// 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 the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// resCursor, resCollection, err = client.HScan(key, initialCursor) +// resCursor = {0 false} +// resCollection = [{a false} {1 false} {b false} {2 false}] +// +// [valkey.io]: https://valkey.io/commands/hscan/ +func (client *baseClient) HScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.HScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the +// command. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. +// options - The [api.HashScanOptions]. +// +// Return value: +// +// An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. +// The second element is always an array of the subset of the set held in `key`. The array in the +// second element is always a flattened series of String pairs, where the key is at even indices +// and the value is at odd indices. +// +// Example: +// +// // Assume key contains a hash {{"a": "1"}, {"b", "2"}} +// opts := options.NewHashScanOptionsBuilder().SetMatch("a") +// resCursor, resCollection, err = client.HScan(key, initialCursor, opts) +// // resCursor = {0 false} +// // resCollection = [{a false} {1 false}] +// // The resCollection only contains the hash map entry that matches with the match option provided with the command +// // input. +// +// [valkey.io]: https://valkey.io/commands/hscan/ func (client *baseClient) HScanWithOptions( key string, cursor string, options *options.HashScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.HScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -841,27 +897,107 @@ func (client *baseClient) SUnion(keys []string) (map[Result[string]]struct{}, er return handleStringSetResponse(result) } -func (client *baseClient) SScan(key string, cursor string) (Result[string], []Result[string], error) { +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// resCursor, resCol, err = client.sscan("key", "0") +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ +func (client *baseClient) SScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.SScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } +// Iterates incrementally over a set. +// +// Note: When in cluster mode, all keys must map to the same hash slot. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the set. +// cursor - The cursor that points to the next iteration of results. +// A value of `"0"` indicates the start of the search. +// For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). +// options - [options.BaseScanOptions] +// +// Return value: +// +// An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and +// for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. +// The second element is always an array of the subset of the set held in `key`. +// +// Example: +// +// // assume "key" contains a set +// resCursor, resCol, err := client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// for resCursor != "0" { +// opts := options.NewBaseScanOptionsBuilder().SetMatch("*") +// resCursor, resCol, err = client.sscan("key", "0", opts) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) +// } +// // Output: +// // Cursor: 48 +// // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] +// // Cursor: 24 +// // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] +// // Cursor: 0 +// // Members: ['47', '122', '1', '53', '10', '14', '80'] +// +// [valkey.io]: https://valkey.io/commands/sscan/ func (client *baseClient) SScanWithOptions( key string, cursor string, options *options.BaseScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.SScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -2321,19 +2457,19 @@ func (client *baseClient) ZScore(key string, member string) (Result[float64], er // // // assume "key" contains a set // resCursor, resCol, err := client.ZScan("key", "0") -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScan("key", resCursor.Value()) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// resCursor, resCol, err = client.ZScan("key", resCursor) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ -func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Result[string], error) { +func (client *baseClient) ZScan(key string, cursor string) (string, []string, error) { result, err := client.executeCommand(C.ZScan, []string{key, cursor}) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -2359,13 +2495,13 @@ func (client *baseClient) ZScan(key string, cursor string) (Result[string], []Re // Example: // // resCursor, resCol, err := client.ZScanWithOptions("key", "0", options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println(resCursor.Value()) -// fmt.Println(resCol.Value()) +// fmt.Println(resCursor) +// fmt.Println(resCol) // for resCursor != "0" { -// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor.Value(), +// resCursor, resCol, err = client.ZScanWithOptions("key", resCursor, // options.NewBaseScanOptionsBuilder().SetMatch("*")) -// fmt.Println("Cursor: ", resCursor.Value()) -// fmt.Println("Members: ", resCol.Value()) +// fmt.Println("Cursor: ", resCursor) +// fmt.Println("Members: ", resCol) // } // // [valkey.io]: https://valkey.io/commands/zscan/ @@ -2373,15 +2509,15 @@ func (client *baseClient) ZScanWithOptions( key string, cursor string, options *options.ZScanOptions, -) (Result[string], []Result[string], error) { +) (string, []string, error) { optionArgs, err := options.ToArgs() if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } result, err := client.executeCommand(C.ZScan, append([]string{key, cursor}, optionArgs...)) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } return handleScanResponse(result) } @@ -2635,3 +2771,141 @@ func (client *baseClient) ZRemRangeByScore(key string, rangeQuery options.RangeB } return handleIntResponse(result) } + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the logarithmic access frequency counter of the +// object stored at key as a long. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectFreq(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-freq/ +func (client *baseClient) ObjectFreq(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectFreq, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the logarithmic access frequency counter of a Valkey object stored at key. +// +// Parameters: +// +// key - The key of the object to get the logarithmic access frequency counter of. +// +// Return value: +// +// If key exists, returns the idle time in seconds. Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectIdleTime(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-idletime/ +func (client *baseClient) ObjectIdleTime(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectIdleTime, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +// Returns the reference count of the object stored at key. +// +// Parameters: +// +// key - The key of the object to get the reference count of. +// +// Return value: +// +// If key exists, returns the reference count of the object stored at key. +// Otherwise, returns `nil`. +// +// Example: +// +// result, err := client.ObjectRefCount(key) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 +// +// [valkey.io]: https://valkey.io/commands/object-refcount/ +func (client *baseClient) ObjectRefCount(key string) (Result[int64], error) { + result, err := client.executeCommand(C.ObjectRefCount, []string{key}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +func (client *baseClient) Sort(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.Sort, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnly(key string) ([]Result[string], error) { + result, err := client.executeCommand(C.SortReadOnly, []string{key}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortReadOnlyWithOptions(key string, options *options.SortOptions) ([]Result[string], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.SortReadOnly, append([]string{key}, optionArgs...)) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +func (client *baseClient) SortStore(key string, destination string) (Result[int64], error) { + result, err := client.executeCommand(C.Sort, []string{key, "STORE", destination}) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} + +func (client *baseClient) SortStoreWithOptions( + key string, + destination string, + options *options.SortOptions, +) (Result[int64], error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key, "STORE", destination}, optionArgs...)) + if err != nil { + return CreateNilInt64Result(), err + } + return handleIntOrNilResponse(result) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 1f15eddd23..345bff2169 100644 --- a/go/api/generic_base_commands.go +++ b/go/api/generic_base_commands.go @@ -2,6 +2,8 @@ package api +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + // Supports commands and transactions for the "Generic Commands" group for standalone and cluster clients. // // See [valkey.io] for details. @@ -527,4 +529,180 @@ type GenericBaseCommands interface { // // [valkey.io]: https://valkey.io/commands/dump/ Dump(key string) (Result[string], error) + + ObjectFreq(key string) (Result[int64], error) + + ObjectIdleTime(key string) (Result[int64], error) + + ObjectRefCount(key string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.Sort("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + Sort(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To store the result into a new key, see the sortStore function. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.Sort("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // result, err := client.SortStore("key","destkey") + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStore(key string, destination string) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and stores the result in + // destination. The sort command can be used to sort elements based on + // different criteria, apply transformations on sorted elements, and store the result in a new key. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // To get the sort result without storing it into a key, see the sort or sortReadOnly function. + // + // Note: + // In cluster mode, if `key` and `destination` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns + // in cluster mode is supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // destination - The key where the sorted result will be stored. + // sortOptions - The SortOptions type. + // + // Return value: + // The number of elements in the sorted key stored at destination. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortStore("key","destkey",options) + // result.Value(): 1 + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (Result[int64], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sortReadOnly command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // result, err := client.SortReadOnly("key") + // result.Value(): [{1 false} {2 false} {3 false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnly(key string) ([]Result[string], error) + + // Sorts the elements in the list, set, or sorted set at key and returns the result. + // The sort command can be used to sort elements based on different criteria and apply + // transformations on sorted elements. + // This command is routed depending on the client's ReadFrom strategy. + // + // Note: + // In cluster mode, if `key` map to different hash slots, the command + // will be split across these slots and executed separately for each. This means the command + // is atomic only at the slot level. If one or more slot-specific requests fail, the entire + // call will return the first encountered error, even though some requests may have succeeded + // while others did not. If this behavior impacts your application logic, consider splitting + // the request into sub-requests per slot to ensure atomicity. + // The use of SortOptions.byPattern and SortOptions.getPatterns in cluster mode is + // supported since Valkey version 8.0. + // + // Parameters: + // key - The key of the list, set, or sorted set to be sorted. + // sortOptions - The SortOptions type. + // + // Return value: + // An Array of sorted elements. + // + // Example: + // + // options := api.NewSortOptions().SetByPattern("weight_*").SetIsAlpha(false).AddGetPattern("object_*").AddGetPattern("#") + // result, err := client.SortReadOnly("key", options) + // result.Value(): [{Object_3 false} {c false} {Object_1 false} {a false} {Object_2 false} {b false}] + // result.IsNil(): false + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortReadOnlyWithOptions(key string, sortOptions *options.SortOptions) ([]Result[string], error) } diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index 0d1e4514f8..23e27adb9d 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -290,63 +290,13 @@ type HashCommands interface { // [valkey.io]: https://valkey.io/commands/hincrbyfloat/ HIncrByFloat(key string, field string, increment float64) (float64, error) - // Iterates fields of Hash types and their associated values. This definition of HSCAN command does not include the - // optional arguments of the command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // 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 the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // resCursor, resCollection, err = client.HScan(key, initialCursor) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false} {b false} {2 false}] - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScan(key string, cursor string) (Result[string], []Result[string], error) - - // Iterates fields of Hash types and their associated values. This definition of HSCAN includes optional arguments of the - // command. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the hash. - // cursor - The cursor that points to the next iteration of results. A value of "0" indicates the start of the search. - // options - The [api.HashScanOptions]. - // - // Return value: - // An array of the cursor and the subset of the hash held by `key`. The first element is always the `cursor` - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the subset. - // The second element is always an array of the subset of the set held in `key`. The array in the - // second element is always a flattened series of String pairs, where the key is at even indices - // and the value is at odd indices. - // - // Example: - // // Assume key contains a hash {{"a": "1"}, {"b", "2"}} - // opts := options.NewHashScanOptionsBuilder().SetMatch("a") - // resCursor, resCollection, err = client.HScan(key, initialCursor, opts) - // // resCursor = {0 false} - // // resCollection = [{a false} {1 false}] - // // The resCollection only contains the hash map entry that matches with the match option provided with the command - // // input. - // - // [valkey.io]: https://valkey.io/commands/hscan/ - HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (Result[string], []Result[string], error) + HScan(key string, cursor string) (string, []string, error) HRandField(key string) (Result[string], error) HRandFieldWithCount(key string, count int64) ([]string, error) HRandFieldWithCountWithValues(key string, count int64) ([][]string, error) + + HScanWithOptions(key string, cursor string, options *options.HashScanOptions) (string, []string, error) } diff --git a/go/api/options/sort_options.go b/go/api/options/sort_options.go new file mode 100644 index 0000000000..6ff883295c --- /dev/null +++ b/go/api/options/sort_options.go @@ -0,0 +1,131 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +const ( + // LIMIT subcommand string to include in the SORT and SORT_RO commands. + LIMIT_COMMAND_STRING = "LIMIT" + // ALPHA subcommand string to include in the SORT and SORT_RO commands. + ALPHA_COMMAND_STRING = "ALPHA" + // BY subcommand string to include in the SORT and SORT_RO commands. + // Supported in cluster mode since Valkey version 8.0 and above. + BY_COMMAND_STRING = "BY" + // GET subcommand string to include in the SORT and SORT_RO commands. + GET_COMMAND_STRING = "GET" +) + +// SortLimit struct represents the range of elements to retrieve +// The LIMIT argument is commonly used to specify a subset of results from the matching elements, similar to the +// LIMIT clause in SQL (e.g., `SELECT LIMIT offset, count`). +type SortLimit struct { + Offset int64 + Count int64 +} + +// OrderBy specifies the order to sort the elements. Can be ASC (ascending) or DESC(descending). +type OrderBy string + +const ( + ASC OrderBy = "ASC" + DESC OrderBy = "DESC" +) + +// SortOptions struct combines both the base options and additional sorting options +type SortOptions struct { + SortLimit *SortLimit + OrderBy OrderBy + IsAlpha bool + ByPattern string + GetPatterns []string +} + +func NewSortOptions() *SortOptions { + return &SortOptions{ + OrderBy: ASC, // Default order is ascending + IsAlpha: false, // Default is numeric sorting + } +} + +// SortLimit Limits the range of elements +// Offset is the starting position of the range, zero based. +// Count is the maximum number of elements to include in the range. +// A negative count returns all elements from the offset. +func (opts *SortOptions) SetSortLimit(offset, count int64) *SortOptions { + opts.SortLimit = &SortLimit{Offset: offset, Count: count} + return opts +} + +// OrderBy sets the order to sort by (ASC or DESC) +func (opts *SortOptions) SetOrderBy(order OrderBy) *SortOptions { + opts.OrderBy = order + return opts +} + +// IsAlpha determines whether to sort lexicographically (true) or numerically (false) +func (opts *SortOptions) SetIsAlpha(isAlpha bool) *SortOptions { + opts.IsAlpha = isAlpha + return opts +} + +// ByPattern - a pattern to sort by external keys instead of by the elements stored at the key themselves. The +// pattern should contain an asterisk (*) as a placeholder for the element values, where the value +// from the key replaces the asterisk to create the key name. For example, if key +// contains IDs of objects, byPattern can be used to sort these IDs based on an +// attribute of the objects, like their weights or timestamps. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) SetByPattern(byPattern string) *SortOptions { + opts.ByPattern = byPattern + return opts +} + +// A pattern used to retrieve external keys' values, instead of the elements at key. +// The pattern should contain an asterisk (*) as a placeholder for the element values, where the +// value from key replaces the asterisk to create the key name. This +// allows the sorted elements to be transformed based on the related keys values. For example, if +// key< contains IDs of users, getPatterns can be used to retrieve +// specific attributes of these users, such as their names or email addresses. E.g., if +// getPatterns is name_*, the command will return the values of the keys +// name_<element> for each sorted element. Multiple getPatterns +// arguments can be provided to retrieve multiple attributes. The special value # can +// be used to include the actual element from key being sorted. If not provided, only +// the sorted elements themselves are returned. +// Supported in cluster mode since Valkey version 8.0 and above. +func (opts *SortOptions) AddGetPattern(getPattern string) *SortOptions { + opts.GetPatterns = append(opts.GetPatterns, getPattern) + return opts +} + +// ToArgs creates the arguments to be used in SORT and SORT_RO commands. +func (opts *SortOptions) ToArgs() []string { + var args []string + + if opts.SortLimit != nil { + args = append( + args, + LIMIT_COMMAND_STRING, + utils.IntToString(opts.SortLimit.Offset), + utils.IntToString(opts.SortLimit.Count), + ) + } + + if opts.OrderBy != "" { + args = append(args, string(opts.OrderBy)) + } + + if opts.IsAlpha { + args = append(args, ALPHA_COMMAND_STRING) + } + + if opts.ByPattern != "" { + args = append(args, BY_COMMAND_STRING, opts.ByPattern) + } + + for _, getPattern := range opts.GetPatterns { + args = append(args, GET_COMMAND_STRING, getPattern) + } + return args +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 1356b52d51..10a04c9ea1 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -488,40 +488,38 @@ func handleKeyWithMemberAndScoreResponse(response *C.struct_CommandResponse) (Re return CreateKeyWithMemberAndScoreResult(KeyWithMemberAndScore{key, member, score}), nil } -func handleScanResponse( - response *C.struct_CommandResponse, -) (Result[string], []Result[string], error) { +func handleScanResponse(response *C.struct_CommandResponse) (string, []string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Array, false) if typeErr != nil { - return CreateNilStringResult(), nil, typeErr + return "", nil, typeErr } slice, err := parseArray(response) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } if arr, ok := slice.([]interface{}); ok { - resCollection, err := convertToResultStringArray(arr[1].([]interface{})) + resCollection, err := convertToStringArray(arr[1].([]interface{})) if err != nil { - return CreateNilStringResult(), nil, err + return "", nil, err } - return CreateStringResult(arr[0].(string)), resCollection, nil + return arr[0].(string), resCollection, nil } - return CreateNilStringResult(), nil, err + return "", nil, err } -func convertToResultStringArray(input []interface{}) ([]Result[string], error) { - result := make([]Result[string], len(input)) +func convertToStringArray(input []interface{}) ([]string, error) { + result := make([]string, len(input)) for i, v := range input { str, ok := v.(string) if !ok { return nil, fmt.Errorf("element at index %d is not a string: %v", i, v) } - result[i] = CreateStringResult(str) + result[i] = str } return result, nil } diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 7e045d96e8..5d2315ae74 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -377,79 +377,9 @@ type SetCommands interface { // [valkey.io]: https://valkey.io/commands/sunion/ SUnion(keys []string) (map[Result[string]]struct{}, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor, resCol, err := client.sscan("key", "0") - // for resCursor != "0" { - // resCursor, resCol, err = client.sscan("key", "0") - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScan(key string, cursor string) (Result[string], []Result[string], error) + SScan(key string, cursor string) (string, []string, error) - // Iterates incrementally over a set. - // - // Note: When in cluster mode, all keys must map to the same hash slot. - // - // See [valkey.io] for details. - // - // Parameters: - // key - The key of the set. - // cursor - The cursor that points to the next iteration of results. - // A value of `"0"` indicates the start of the search. - // For Valkey 8.0 and above, negative cursors are treated like the initial cursor("0"). - // options - [options.BaseScanOptions] - // - // Return value: - // An array of the cursor and the subset of the set held by `key`. The first element is always the `cursor` and - // for the next iteration of results. The `cursor` will be `"0"` on the last iteration of the set. - // The second element is always an array of the subset of the set held in `key`. - // - // Example: - // // assume "key" contains a set - // resCursor resCol, err := client.sscan("key", "0", opts) - // for resCursor != "0" { - // opts := options.NewBaseScanOptionsBuilder().SetMatch("*") - // resCursor, resCol, err = client.sscan("key", "0", opts) - // fmt.Println("Cursor: ", resCursor.Value()) - // fmt.Println("Members: ", resCol.Value()) - // } - // // Output: - // // Cursor: 48 - // // Members: ['3', '118', '120', '86', '76', '13', '61', '111', '55', '45'] - // // Cursor: 24 - // // Members: ['38', '109', '11', '119', '34', '24', '40', '57', '20', '17'] - // // Cursor: 0 - // // Members: ['47', '122', '1', '53', '10', '14', '80'] - // - // [valkey.io]: https://valkey.io/commands/sscan/ - SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (Result[string], []Result[string], error) + SScanWithOptions(key string, cursor string, options *options.BaseScanOptions) (string, []string, error) // Moves `member` from the set at `source` to the set at `destination`, removing it from the source set. // Creates a new destination set if needed. The operation is atomic. diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 237a8d3489..47b505a558 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -382,9 +382,9 @@ type SortedSetCommands interface { ZCount(key string, rangeOptions *options.ZCountRange) (int64, error) - ZScan(key string, cursor string) (Result[string], []Result[string], error) + ZScan(key string, cursor string) (string, []string, error) - ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (Result[string], []Result[string], error) + ZScanWithOptions(key string, cursor string, options *options.ZScanOptions) (string, []string, error) ZRemRangeByLex(key string, rangeQuery options.RangeByLex) (int64, error) diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index ce184a3f92..779ca6d9b7 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -1139,7 +1139,7 @@ func (suite *GlideTestSuite) TestHScan() { // Check for empty set. resCursor, resCollection, err := client.HScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // Negative cursor check. @@ -1148,7 +1148,7 @@ func (suite *GlideTestSuite) TestHScan() { assert.NotEmpty(t, err) } else { resCursor, resCollection, _ = client.HScan(key1, "-1") - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } @@ -1157,27 +1157,27 @@ func (suite *GlideTestSuite) TestHScan() { assert.Equal(t, int64(len(charMembers)), hsetResult) resCursor, resCollection, _ = client.HScan(key1, initialCursor) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) // Length includes the score which is twice the map size assert.Equal(t, len(charMap)*2, len(resCollection)) - resultKeys := make([]api.Result[string], 0) - resultValues := make([]api.Result[string], 0) + resultKeys := make([]string, 0) + resultValues := make([]string, 0) for i := 0; i < len(resCollection); i += 2 { resultKeys = append(resultKeys, resCollection[i]) resultValues = append(resultValues, resCollection[i+1]) } - keysList, valuesList := convertMapKeysAndValuesToResultList(charMap) + keysList, valuesList := convertMapKeysAndValuesToLists(charMap) assert.True(t, isSubset(resultKeys, keysList) && isSubset(keysList, resultKeys)) assert.True(t, isSubset(resultValues, valuesList) && isSubset(valuesList, resultValues)) opts := options.NewHashScanOptionsBuilder().SetMatch("a") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(resCollection), 2) - assert.Equal(t, resCollection[0].Value(), "a") - assert.Equal(t, resCollection[1].Value(), "0") + assert.Equal(t, resCollection[0], "a") + assert.Equal(t, resCollection[1], "0") // Result contains a subset of the key combinedMap := make(map[string]string) @@ -1191,12 +1191,12 @@ func (suite *GlideTestSuite) TestHScan() { hsetResult, _ = client.HSet(key1, combinedMap) assert.Equal(t, int64(len(numberMap)), hsetResult) resultCursor := "0" - secondResultAllKeys := make([]api.Result[string], 0) - secondResultAllValues := make([]api.Result[string], 0) + secondResultAllKeys := make([]string, 0) + secondResultAllValues := make([]string, 0) isFirstLoop := true for { resCursor, resCollection, _ = client.HScan(key1, resultCursor) - resultCursor = resCursor.Value() + resultCursor = resCursor for i := 0; i < len(resCollection); i += 2 { secondResultAllKeys = append(secondResultAllKeys, resCollection[i]) secondResultAllValues = append(secondResultAllValues, resCollection[i+1]) @@ -1211,7 +1211,7 @@ func (suite *GlideTestSuite) TestHScan() { // Scan with result cursor to get the next set of data. newResultCursor, secondResult, _ := client.HScan(key1, resultCursor) assert.NotEqual(t, resultCursor, newResultCursor) - resultCursor = newResultCursor.Value() + resultCursor = newResultCursor assert.False(t, reflect.DeepEqual(secondResult, resCollection)) for i := 0; i < len(secondResult); i += 2 { secondResultAllKeys = append(secondResultAllKeys, secondResult[i]) @@ -1223,41 +1223,41 @@ func (suite *GlideTestSuite) TestHScan() { break } } - numberKeysList, numberValuesList := convertMapKeysAndValuesToResultList(numberMap) + numberKeysList, numberValuesList := convertMapKeysAndValuesToLists(numberMap) assert.True(t, isSubset(numberKeysList, secondResultAllKeys)) assert.True(t, isSubset(numberValuesList, secondResultAllValues)) // Test match pattern opts = options.NewHashScanOptionsBuilder().SetMatch("*") resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ := strconv.Atoi(resCursor.Value()) + resCursorInt, _ := strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, int(len(resCollection)) >= defaultCount) // Test count opts = options.NewHashScanOptionsBuilder().SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 20) // Test count with match returns a non-empty list opts = options.NewHashScanOptionsBuilder().SetMatch("1*").SetCount(int64(20)) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) assert.True(t, len(resCollection) >= 0) if suite.serverVersion >= "8.0.0" { opts = options.NewHashScanOptionsBuilder().SetNoValue(true) resCursor, resCollection, _ = client.HScanWithOptions(key1, initialCursor, opts) - resCursorInt, _ = strconv.Atoi(resCursor.Value()) + resCursorInt, _ = strconv.Atoi(resCursor) assert.True(t, resCursorInt >= 0) // Check if all fields don't start with "num" containsElementsWithNumKeyword := false for i := 0; i < len(resCollection); i++ { - if strings.Contains(resCollection[i].Value(), "num") { + if strings.Contains(resCollection[i], "num") { containsElementsWithNumKeyword = true break } @@ -2388,34 +2388,27 @@ func (suite *GlideTestSuite) TestSScan() { defaultCount := 10 // use large dataset to force an iterative cursor. numMembers := make([]string, 50000) - numMembersResult := make([]api.Result[string], 50000) + numMembersResult := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } t := suite.T() // populate the dataset slice for i := 0; i < 50000; i++ { numMembers[i] = strconv.Itoa(i) - numMembersResult[i] = api.CreateStringResult(strconv.Itoa(i)) + numMembersResult[i] = strconv.Itoa(i) } // empty set resCursor, resCollection, err := client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) // negative cursor if suite.serverVersion < "8.0.0" { resCursor, resCollection, err = client.SScan(key1, "-1") assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Empty(t, resCollection) } else { _, _, err = client.SScan(key1, "-1") @@ -2429,15 +2422,15 @@ func (suite *GlideTestSuite) TestSScan() { assert.Equal(t, int64(len(charMembers)), res) resCursor, resCollection, err = client.SScan(key1, initialCursor) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) + assert.Equal(t, initialCursor, resCursor) assert.Equal(t, len(charMembers), len(resCollection)) - assert.True(t, isSubset(resCollection, charMembersResult)) + assert.True(t, isSubset(resCollection, charMembers)) opts := options.NewBaseScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.Equal(t, initialCursor, resCursor.Value()) - assert.True(t, isSubset(resCollection, []api.Result[string]{api.CreateStringResult("a")})) + assert.Equal(t, initialCursor, resCursor) + assert.True(t, isSubset(resCollection, []string{"a"})) // result contains a subset of the key res, err = client.SAdd(key1, numMembers) @@ -2448,8 +2441,8 @@ func (suite *GlideTestSuite) TestSScan() { resultCollection := resCollection // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.SScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.SScan(key1, resCursor) assert.NoError(t, err) assert.NotEqual(t, nextCursor, resCursor) assert.False(t, isSubset(resultCollection, nextCol)) @@ -2458,27 +2451,27 @@ func (suite *GlideTestSuite) TestSScan() { } assert.NotEmpty(t, resultCollection) assert.True(t, isSubset(numMembersResult, resultCollection)) - assert.True(t, isSubset(charMembersResult, resultCollection)) + assert.True(t, isSubset(charMembers, resultCollection)) // test match pattern opts = options.NewBaseScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), defaultCount) // test count opts = options.NewBaseScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewBaseScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.SScanWithOptions(key1, initialCursor, opts) assert.NoError(t, err) - assert.NotEqual(t, initialCursor, resCursor.Value()) + assert.NotEqual(t, initialCursor, resCursor) assert.GreaterOrEqual(t, len(resCollection), 0) // exceptions @@ -3891,6 +3884,225 @@ func (suite *GlideTestSuite) TestPfCount_NoExistingKeys() { }) } +func (suite *GlideTestSuite) TestSortWithOptions_AscendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(true) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("a"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSort_SuccessfulSort() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.Sort(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortStore_BasicSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "2", "5", "1", "4"}) + + result, err := client.SortStore(key, sortedKey) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("4"), + api.CreateStringResult("5"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStore_ErrorHandling() { + suite.runWithDefaultClients(func(client api.BaseClient) { + result, err := client.SortStore("{listKey}nonExistingKey", "{listKey}mydestinationKey") + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), int64(0), result.Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_DescendingOrder() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{key}" + uuid.New().String() + sortedKey := "{key}" + uuid.New().String() + client.LPush(key, []string{"30", "20", "10", "40", "50"}) + + options := options.NewSortOptions().SetOrderBy(options.DESC).SetIsAlpha(false) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("50"), + api.CreateStringResult("40"), + api.CreateStringResult("30"), + api.CreateStringResult("20"), + api.CreateStringResult("10"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_AlphaSorting() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"apple", "banana", "cherry", "date", "elderberry"}) + + options := options.NewSortOptions().SetIsAlpha(true) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("apple"), + api.CreateStringResult("banana"), + api.CreateStringResult("cherry"), + api.CreateStringResult("date"), + api.CreateStringResult("elderberry"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_Limit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"10", "20", "30", "40", "50"}) + + options := options.NewSortOptions().SetSortLimit(1, 3) + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(3), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("20"), + api.CreateStringResult("30"), + api.CreateStringResult("40"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestSortReadOnly_SuccessfulSort() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"3", "1", "2"}) + + sortResult, err := client.SortReadOnly(key) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("1"), + api.CreateStringResult("2"), + api.CreateStringResult("3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortReadyOnlyWithOptions_DescendingOrder() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"b", "a", "c"}) + + options := options.NewSortOptions(). + SetOrderBy(options.DESC). + SetIsAlpha(true). + SetSortLimit(0, 3) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("c"), + api.CreateStringResult("b"), + api.CreateStringResult("a"), + } + assert.Equal(suite.T(), resultList, sortResult) + }) +} + func (suite *GlideTestSuite) TestBLMove() { if suite.serverVersion < "6.2.0" { suite.T().Skip("This feature is added in version 6.2.0") @@ -5380,30 +5592,23 @@ func (suite *GlideTestSuite) TestZScan() { // Set up test data - use a large number of entries to force an iterative cursor numberMap := make(map[string]float64) - numMembersResult := make([]api.Result[string], 50000) + numMembers := make([]string, 50000) charMembers := []string{"a", "b", "c", "d", "e"} - charMembersResult := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - api.CreateStringResult("d"), - api.CreateStringResult("e"), - } for i := 0; i < 50000; i++ { numberMap["member"+strconv.Itoa(i)] = float64(i) - numMembersResult[i] = api.CreateStringResult("member" + strconv.Itoa(i)) + numMembers[i] = "member" + strconv.Itoa(i) } charMap := make(map[string]float64) - charMapValues := []api.Result[string]{} + charMapValues := []string{} for i, val := range charMembers { charMap[val] = float64(i) - charMapValues = append(charMapValues, api.CreateStringResult(strconv.Itoa(i))) + charMapValues = append(charMapValues, strconv.Itoa(i)) } // Empty set resCursor, resCollection, err := client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) // Negative cursor @@ -5414,7 +5619,7 @@ func (suite *GlideTestSuite) TestZScan() { } else { resCursor, resCollection, err = client.ZScan(key1, "-1") assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Empty(suite.T(), resCollection) } @@ -5425,11 +5630,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, initialCursor) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) + assert.Equal(suite.T(), initialCursor, resCursor) assert.Equal(suite.T(), len(charMap)*2, len(resCollection)) - resultKeySet := make([]api.Result[string], 0, len(charMap)) - resultValueSet := make([]api.Result[string], 0, len(charMap)) + resultKeySet := make([]string, 0, len(charMap)) + resultValueSet := make([]string, 0, len(charMap)) // Iterate through array taking pairs of items for i := 0; i < len(resCollection); i += 2 { @@ -5438,7 +5643,7 @@ func (suite *GlideTestSuite) TestZScan() { } // Verify all expected keys exist in result - assert.True(suite.T(), isSubset(charMembersResult, resultKeySet)) + assert.True(suite.T(), isSubset(charMembers, resultKeySet)) // Scores come back as integers converted to a string when the fraction is zero. assert.True(suite.T(), isSubset(charMapValues, resultValueSet)) @@ -5446,8 +5651,8 @@ func (suite *GlideTestSuite) TestZScan() { opts := options.NewZScanOptionsBuilder().SetMatch("a") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), initialCursor, resCursor.Value()) - assert.Equal(suite.T(), resCollection, []api.Result[string]{api.CreateStringResult("a"), api.CreateStringResult("0")}) + assert.Equal(suite.T(), initialCursor, resCursor) + assert.Equal(suite.T(), resCollection, []string{"a", "0"}) // Result contains a subset of the key res, err = client.ZAdd(key1, numberMap) @@ -5457,11 +5662,11 @@ func (suite *GlideTestSuite) TestZScan() { resCursor, resCollection, err = client.ZScan(key1, "0") assert.NoError(suite.T(), err) resultCollection := resCollection - resKeys := []api.Result[string]{} + resKeys := []string{} // 0 is returned for the cursor of the last iteration - for resCursor.Value() != "0" { - nextCursor, nextCol, err := client.ZScan(key1, resCursor.Value()) + for resCursor != "0" { + nextCursor, nextCol, err := client.ZScan(key1, resCursor) assert.NoError(suite.T(), err) assert.NotEqual(suite.T(), nextCursor, resCursor) assert.False(suite.T(), isSubset(resultCollection, nextCol)) @@ -5475,27 +5680,27 @@ func (suite *GlideTestSuite) TestZScan() { assert.NotEmpty(suite.T(), resultCollection) // Verify we got all keys and values - assert.True(suite.T(), isSubset(numMembersResult, resKeys)) + assert.True(suite.T(), isSubset(numMembers, resKeys)) // Test match pattern opts = options.NewZScanOptionsBuilder().SetMatch("*") resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), defaultCount) // test count opts = options.NewZScanOptionsBuilder().SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 20) // test count with match, returns a non-empty array opts = options.NewZScanOptionsBuilder().SetMatch("1*").SetCount(20) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - assert.NotEqual(suite.T(), initialCursor, resCursor.Value()) + assert.NotEqual(suite.T(), initialCursor, resCursor) assert.GreaterOrEqual(suite.T(), len(resCollection), 0) // Test NoScores option for Redis 8.0.0+ @@ -5503,13 +5708,13 @@ func (suite *GlideTestSuite) TestZScan() { opts = options.NewZScanOptionsBuilder().SetNoScores(true) resCursor, resCollection, err = client.ZScanWithOptions(key1, initialCursor, opts) assert.NoError(suite.T(), err) - cursor, err := strconv.ParseInt(resCursor.Value(), 10, 64) + cursor, err := strconv.ParseInt(resCursor, 10, 64) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), cursor, int64(0)) // Verify all fields start with "member" for _, field := range resCollection { - assert.True(suite.T(), strings.HasPrefix(field.Value(), "member")) + assert.True(suite.T(), strings.HasPrefix(field, "member")) } } @@ -6316,3 +6521,222 @@ func (suite *GlideTestSuite) TestZRemRangeByScore() { assert.IsType(suite.T(), &api.RequestError{}, err) }) } + +func (suite *GlideTestSuite) TestObjectIdleTime() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + sleepSec := int64(5) + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "noeviction", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("noeviction") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultIdleTime, err := defaultClient.ObjectIdleTime(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultIdleTime.Value(), sleepSec) + }) +} + +func (suite *GlideTestSuite) TestObjectRefCount() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(client.Set(key, value)) + resultGetRestoreKey, err := client.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGetRestoreKey.Value()) + resultObjectRefCount, err := client.ObjectRefCount(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjectRefCount.Value(), int64(1)) + }) +} + +func (suite *GlideTestSuite) TestObjectFreq() { + suite.runWithDefaultClients(func(client api.BaseClient) { + defaultClient := suite.defaultClient() + key := "testKey1_" + uuid.New().String() + value := "hello" + t := suite.T() + suite.verifyOK(defaultClient.Set(key, value)) + keyValueMap := map[string]string{ + "maxmemory-policy": "volatile-lfu", + } + suite.verifyOK(defaultClient.ConfigSet(keyValueMap)) + key1 := api.CreateStringResult("maxmemory-policy") + value1 := api.CreateStringResult("volatile-lfu") + resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1} + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, resultConfigMap, resultConfig, "Configuration mismatch for maxmemory-policy") + sleepSec := int64(5) + time.Sleep(time.Duration(sleepSec) * time.Second) + resultGet, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet.Value()) + resultGet2, err := defaultClient.Get(key) + assert.Nil(t, err) + assert.Equal(t, value, resultGet2.Value()) + resultObjFreq, err := defaultClient.ObjectFreq(key) + assert.Nil(t, err) + assert.GreaterOrEqual(t, resultObjFreq.Value(), int64(2)) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_ExternalWeights() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_GetPatterns() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) + }) +} + +func (suite *GlideTestSuite) TestSortWithOptions_SuccessfulSortByWeightAndGet() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) + }) +} + +func (suite *GlideTestSuite) TestSortStoreWithOptions_ByPattern() { + suite.SkipIfServerVersionLowerThanBy("8.1.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := "{listKey}" + uuid.New().String() + sortedKey := "{listKey}" + uuid.New().String() + client.LPush(key, []string{"a", "b", "c", "d", "e"}) + client.Set("{listKey}weight_a", "5") + client.Set("{listKey}weight_b", "2") + client.Set("{listKey}weight_c", "3") + client.Set("{listKey}weight_d", "1") + client.Set("{listKey}weight_e", "4") + + options := options.NewSortOptions().SetByPattern("{listKey}weight_*") + + result, err := client.SortStoreWithOptions(key, sortedKey, options) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), result) + assert.Equal(suite.T(), int64(5), result.Value()) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []api.Result[string]{ + api.CreateStringResult("d"), + api.CreateStringResult("b"), + api.CreateStringResult("c"), + api.CreateStringResult("e"), + api.CreateStringResult("a"), + } + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), resultList, sortedValues) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 063e884a5d..3c298f1ee6 100644 --- a/go/integTest/standalone_commands_test.go +++ b/go/integTest/standalone_commands_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/valkey-io/valkey-glide/go/glide/api" + "github.com/valkey-io/valkey-glide/go/glide/api/options" "github.com/stretchr/testify/assert" ) @@ -273,3 +274,117 @@ func (suite *GlideTestSuite) TestSelect_SwitchBetweenDatabases() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), value2, result.Value()) } + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_ExternalWeights() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "3") + client.Set("weight_item2", "1") + client.Set("weight_item3", "2") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false) + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + resultList := []api.Result[string]{ + api.CreateStringResult("item2"), + api.CreateStringResult("item3"), + api.CreateStringResult("item1"), + } + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_GetPatterns() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("object_item1", "Object_1") + client.Set("object_item2", "Object_2") + client.Set("object_item3", "Object_3") + + options := options.NewSortOptions(). + SetByPattern("weight_*"). + SetOrderBy(options.ASC). + SetIsAlpha(false). + AddGetPattern("object_*") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object_2"), + api.CreateStringResult("Object_3"), + api.CreateStringResult("Object_1"), + } + + assert.Equal(suite.T(), resultList, sortResult) +} + +func (suite *GlideTestSuite) TestSortReadOnlyWithOptions_SuccessfulSortByWeightAndGet() { + client := suite.defaultClient() + if suite.serverVersion < "7.0.0" { + suite.T().Skip("This feature is added in version 7") + } + key := uuid.New().String() + client.LPush(key, []string{"item1", "item2", "item3"}) + + client.Set("weight_item1", "10") + client.Set("weight_item2", "5") + client.Set("weight_item3", "15") + + client.Set("object_item1", "Object 1") + client.Set("object_item2", "Object 2") + client.Set("object_item3", "Object 3") + + options := options.NewSortOptions(). + SetOrderBy(options.ASC). + SetIsAlpha(false). + SetByPattern("weight_*"). + AddGetPattern("object_*"). + AddGetPattern("#") + + sortResult, err := client.SortReadOnlyWithOptions(key, options) + + assert.Nil(suite.T(), err) + + resultList := []api.Result[string]{ + api.CreateStringResult("Object 2"), + api.CreateStringResult("item2"), + api.CreateStringResult("Object 1"), + api.CreateStringResult("item1"), + api.CreateStringResult("Object 3"), + api.CreateStringResult("item3"), + } + + assert.Equal(suite.T(), resultList, sortResult) + + objectItem2, err := client.Get("object_item2") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 2", objectItem2.Value()) + + objectItem1, err := client.Get("object_item1") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 1", objectItem1.Value()) + + objectItem3, err := client.Get("object_item3") + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "Object 3", objectItem3.Value()) + + assert.Equal(suite.T(), "item2", sortResult[1].Value()) + assert.Equal(suite.T(), "item1", sortResult[3].Value()) + assert.Equal(suite.T(), "item3", sortResult[5].Value()) +} diff --git a/go/integTest/test_utils.go b/go/integTest/test_utils.go index 144d019dfc..8e7b37bb8f 100644 --- a/go/integTest/test_utils.go +++ b/go/integTest/test_utils.go @@ -2,28 +2,26 @@ package integTest -import "github.com/valkey-io/valkey-glide/go/glide/api" - // check if sliceA is a subset of sliceB -func isSubset(sliceA []api.Result[string], sliceB []api.Result[string]) bool { - setB := make(map[string]struct{}) +func isSubset[T comparable](sliceA []T, sliceB []T) bool { + setB := make(map[T]struct{}) for _, v := range sliceB { - setB[v.Value()] = struct{}{} + setB[v] = struct{}{} } for _, v := range sliceA { - if _, found := setB[v.Value()]; !found { + if _, found := setB[v]; !found { return false } } return true } -func convertMapKeysAndValuesToResultList(m map[string]string) ([]api.Result[string], []api.Result[string]) { - keys := make([]api.Result[string], 0) - values := make([]api.Result[string], 0) +func convertMapKeysAndValuesToLists(m map[string]string) ([]string, []string) { + keys := make([]string, 0) + values := make([]string, 0) for key, value := range m { - keys = append(keys, api.CreateStringResult(key)) - values = append(values, api.CreateStringResult(value)) + keys = append(keys, key) + values = append(values, value) } return keys, values }