diff --git a/CHANGELOG.md b/CHANGELOG.md index 299180a2a7..951db65fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ * Go: Add `BZPopMin` ([#2849](https://github.com/valkey-io/valkey-glide/pull/2849)) * Java: Shadow `protobuf` dependency ([#2931](https://github.com/valkey-io/valkey-glide/pull/2931)) * Java: Add `RESP2` support ([#2383](https://github.com/valkey-io/valkey-glide/pull/2383)) -* Node: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909)) +* Node, Python: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909), [#2962](https://github.com/valkey-io/valkey-glide/pull/2962)) #### Breaking Changes diff --git a/go/api/base_client.go b/go/api/base_client.go index 96f75d5d49..e28c28a4c7 100644 --- a/go/api/base_client.go +++ b/go/api/base_client.go @@ -33,6 +33,7 @@ type BaseClient interface { ConnectionManagementCommands HyperLogLogCommands GenericBaseCommands + BitmapCommands // Close terminates the client by closing all associated resources. Close() } @@ -258,7 +259,7 @@ func (client *baseClient) MGet(keys []string) ([]Result[string], error) { return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) Incr(key string) (int64, error) { @@ -376,7 +377,7 @@ func (client *baseClient) HGet(key string, field string) (Result[string], error) return handleStringOrNilResponse(result) } -func (client *baseClient) HGetAll(key string) (map[Result[string]]Result[string], error) { +func (client *baseClient) HGetAll(key string) (map[string]string, error) { result, err := client.executeCommand(C.HGetAll, []string{key}) if err != nil { return nil, err @@ -391,7 +392,7 @@ func (client *baseClient) HMGet(key string, fields []string) ([]Result[string], return nil, err } - return handleStringArrayResponse(result) + return handleStringOrNilArrayResponse(result) } func (client *baseClient) HSet(key string, values map[string]string) (int64, error) { @@ -430,7 +431,7 @@ func (client *baseClient) HLen(key string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) HVals(key string) ([]Result[string], error) { +func (client *baseClient) HVals(key string) ([]string, error) { result, err := client.executeCommand(C.HVals, []string{key}) if err != nil { return nil, err @@ -448,7 +449,7 @@ func (client *baseClient) HExists(key string, field string) (bool, error) { return handleBoolResponse(result) } -func (client *baseClient) HKeys(key string) ([]Result[string], error) { +func (client *baseClient) HKeys(key string) ([]string, error) { result, err := client.executeCommand(C.HKeys, []string{key}) if err != nil { return nil, err @@ -565,6 +566,106 @@ func (client *baseClient) HScanWithOptions( return handleScanResponse(result) } +// Returns a random field name from the hash value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// +// Return value: +// +// A random field name from the hash stored at `key`, or `nil` when +// the key does not exist. +// +// Example: +// +// field, err := client.HRandField("my_hash") +// +// [valkey.io]: https://valkey.io/commands/hrandfield/ +func (client *baseClient) HRandField(key string) (Result[string], error) { + result, err := client.executeCommand(C.HRandField, []string{key}) + if err != nil { + return CreateNilStringResult(), err + } + return handleStringOrNilResponse(result) +} + +// Retrieves up to `count` random field names from the hash value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// An array of random field names from the hash stored at `key`, +// or an empty array when the key does not exist. +// +// Example: +// +// fields, err := client.HRandFieldWithCount("my_hash", -5) +// +// [valkey.io]: https://valkey.io/commands/hrandfield/ +func (client *baseClient) HRandFieldWithCount(key string, count int64) ([]string, error) { + result, err := client.executeCommand(C.HRandField, []string{key, utils.IntToString(count)}) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} + +// Retrieves up to `count` random field names along with their values from the hash +// value stored at `key`. +// +// Since: +// +// Valkey 6.2.0 and above. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the hash. +// count - The number of field names to return. +// If `count` is positive, returns unique elements. If negative, allows for duplicates. +// +// Return value: +// +// A 2D `array` of `[field, value]` arrays, where `field` is a random +// field name from the hash and `value` is the associated value of the field name. +// If the hash does not exist or is empty, the response will be an empty array. +// +// Example: +// +// fieldsAndValues, err := client.HRandFieldWithCountWithValues("my_hash", -5) +// for _, pair := range fieldsAndValues { +// field := pair[0] +// value := pair[1] +// } +// +// [valkey.io]: https://valkey.io/commands/hrandfield/ +func (client *baseClient) HRandFieldWithCountWithValues(key string, count int64) ([][]string, error) { + result, err := client.executeCommand(C.HRandField, []string{key, utils.IntToString(count), options.WithValues}) + if err != nil { + return nil, err + } + return handle2DStringArrayResponse(result) +} + func (client *baseClient) LPush(key string, elements []string) (int64, error) { result, err := client.executeCommand(C.LPush, append([]string{key}, elements...)) if err != nil { @@ -583,13 +684,13 @@ func (client *baseClient) LPop(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -func (client *baseClient) LPopCount(key string, count int64) ([]Result[string], error) { +func (client *baseClient) LPopCount(key string, count int64) ([]string, error) { result, err := client.executeCommand(C.LPop, []string{key, utils.IntToString(count)}) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) LPos(key string, element string) (Result[int64], error) { @@ -610,7 +711,7 @@ func (client *baseClient) LPosWithOptions(key string, element string, options *L return handleIntOrNilResponse(result) } -func (client *baseClient) LPosCount(key string, element string, count int64) ([]Result[int64], error) { +func (client *baseClient) LPosCount(key string, element string, count int64) ([]int64, error) { result, err := client.executeCommand(C.LPos, []string{key, element, CountKeyword, utils.IntToString(count)}) if err != nil { return nil, err @@ -624,7 +725,7 @@ func (client *baseClient) LPosCountWithOptions( element string, count int64, options *LPosOptions, -) ([]Result[int64], error) { +) ([]int64, error) { result, err := client.executeCommand( C.LPos, append([]string{key, element, CountKeyword, utils.IntToString(count)}, options.toArgs()...), @@ -672,7 +773,7 @@ func (client *baseClient) SUnionStore(destination string, keys []string) (int64, return handleIntResponse(result) } -func (client *baseClient) SMembers(key string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SMembers(key string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SMembers, []string{key}) if err != nil { return nil, err @@ -699,7 +800,7 @@ func (client *baseClient) SIsMember(key string, member string) (bool, error) { return handleBoolResponse(result) } -func (client *baseClient) SDiff(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SDiff(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SDiff, keys) if err != nil { return nil, err @@ -717,7 +818,7 @@ func (client *baseClient) SDiffStore(destination string, keys []string) (int64, return handleIntResponse(result) } -func (client *baseClient) SInter(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SInter(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SInter, keys) if err != nil { return nil, err @@ -782,7 +883,7 @@ func (client *baseClient) SMIsMember(key string, members []string) ([]bool, erro return handleBoolArrayResponse(result) } -func (client *baseClient) SUnion(keys []string) (map[Result[string]]struct{}, error) { +func (client *baseClient) SUnion(keys []string) (map[string]struct{}, error) { result, err := client.executeCommand(C.SUnion, keys) if err != nil { return nil, err @@ -904,7 +1005,7 @@ func (client *baseClient) SMove(source string, destination string, member string return handleBoolResponse(result) } -func (client *baseClient) LRange(key string, start int64, end int64) ([]Result[string], error) { +func (client *baseClient) LRange(key string, start int64, end int64) ([]string, error) { result, err := client.executeCommand(C.LRange, []string{key, utils.IntToString(start), utils.IntToString(end)}) if err != nil { return nil, err @@ -958,13 +1059,13 @@ func (client *baseClient) RPop(key string) (Result[string], error) { return handleStringOrNilResponse(result) } -func (client *baseClient) RPopCount(key string, count int64) ([]Result[string], error) { +func (client *baseClient) RPopCount(key string, count int64) ([]string, error) { result, err := client.executeCommand(C.RPop, []string{key, utils.IntToString(count)}) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) LInsert( @@ -989,22 +1090,22 @@ func (client *baseClient) LInsert( return handleIntResponse(result) } -func (client *baseClient) BLPop(keys []string, timeoutSecs float64) ([]Result[string], error) { +func (client *baseClient) BLPop(keys []string, timeoutSecs float64) ([]string, error) { result, err := client.executeCommand(C.BLPop, append(keys, utils.FloatToString(timeoutSecs))) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } -func (client *baseClient) BRPop(keys []string, timeoutSecs float64) ([]Result[string], error) { +func (client *baseClient) BRPop(keys []string, timeoutSecs float64) ([]string, error) { result, err := client.executeCommand(C.BRPop, append(keys, utils.FloatToString(timeoutSecs))) if err != nil { return nil, err } - return handleStringArrayOrNullResponse(result) + return handleStringArrayOrNilResponse(result) } func (client *baseClient) RPushX(key string, elements []string) (int64, error) { @@ -1025,7 +1126,7 @@ func (client *baseClient) LPushX(key string, elements []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map[Result[string]][]Result[string], error) { +func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1046,14 +1147,14 @@ func (client *baseClient) LMPop(keys []string, listDirection ListDirection) (map return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) LMPopCount( keys []string, listDirection ListDirection, count int64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1074,14 +1175,14 @@ func (client *baseClient) LMPopCount( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) BLMPop( keys []string, listDirection ListDirection, timeoutSecs float64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1102,7 +1203,7 @@ func (client *baseClient) BLMPop( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) BLMPopCount( @@ -1110,7 +1211,7 @@ func (client *baseClient) BLMPopCount( listDirection ListDirection, count int64, timeoutSecs float64, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { listDirectionStr, err := listDirection.toString() if err != nil { return nil, err @@ -1131,7 +1232,7 @@ func (client *baseClient) BLMPopCount( return nil, err } - return handleStringToStringArrayMapOrNullResponse(result) + return handleStringToStringArrayMapOrNilResponse(result) } func (client *baseClient) LSet(key string, index int64, element string) (string, error) { @@ -1394,12 +1495,12 @@ func (client *baseClient) Unlink(keys []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) Type(key string) (Result[string], error) { +func (client *baseClient) Type(key string) (string, error) { result, err := client.executeCommand(C.Type, []string{key}) if err != nil { - return CreateNilStringResult(), err + return defaultStringResponse, err } - return handleStringOrNilResponse(result) + return handleStringResponse(result) } func (client *baseClient) Touch(keys []string) (int64, error) { @@ -1411,12 +1512,12 @@ func (client *baseClient) Touch(keys []string) (int64, error) { return handleIntResponse(result) } -func (client *baseClient) Rename(key string, newKey string) (Result[string], error) { +func (client *baseClient) Rename(key string, newKey string) (string, error) { result, err := client.executeCommand(C.Rename, []string{key, newKey}) if err != nil { - return CreateNilStringResult(), err + return defaultStringResponse, err } - return handleStringOrNilResponse(result) + return handleStringResponse(result) } func (client *baseClient) Renamenx(key string, newKey string) (bool, error) { @@ -1561,7 +1662,7 @@ func (client *baseClient) XReadWithOptions( // // Return value: // A `map[string]map[string][][]string` of stream keys to a map of stream entry IDs mapped to an array entries or `nil` if -// a key does not exist or does not contain requiested entries. +// a key does not exist or does not contain requested entries. // // For example: // @@ -1754,7 +1855,7 @@ func (client *baseClient) ZIncrBy(key string, increment float64, member string) return handleFloatResponse(result) } -func (client *baseClient) ZPopMin(key string) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMin(key string) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMin, []string{key}) if err != nil { return nil, err @@ -1762,7 +1863,7 @@ func (client *baseClient) ZPopMin(key string) (map[Result[string]]Result[float64 return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMin, []string{key, utils.IntToString(count)}) if err != nil { return nil, err @@ -1770,7 +1871,7 @@ func (client *baseClient) ZPopMinWithCount(key string, count int64) (map[Result[ return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMax(key string) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMax(key string) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMax, []string{key}) if err != nil { return nil, err @@ -1778,7 +1879,7 @@ func (client *baseClient) ZPopMax(key string) (map[Result[string]]Result[float64 return handleStringDoubleMapResponse(result) } -func (client *baseClient) ZPopMaxWithCount(key string, count int64) (map[Result[string]]Result[float64], error) { +func (client *baseClient) ZPopMaxWithCount(key string, count int64) (map[string]float64, error) { result, err := client.executeCommand(C.ZPopMax, []string{key, utils.IntToString(count)}) if err != nil { return nil, err @@ -1838,16 +1939,15 @@ func (client *baseClient) BZPopMin(keys []string, timeoutSecs float64) (Result[K // result, err := client.ZRange("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) // // // Retrieve members within a score range in descending order -// -// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), -// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). -// -// .SetReverse() +// query := options.NewRangeByScoreQuery( +// options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). +// SetReverse() // result, err := client.ZRange("my_sorted_set", query) // // `result` contains members which have scores within the range of negative infinity to 3, in descending order // // [valkey.io]: https://valkey.io/commands/zrange/ -func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) { +func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) { args := make([]string, 0, 10) args = append(args, key) args = append(args, rangeQuery.ToArgs()...) @@ -1882,10 +1982,9 @@ func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([] // result, err := client.ZRangeWithScores("my_sorted_set", options.NewRangeByIndexQuery(0, -1)) // // // Retrieve members within a score range in descending order -// -// query := options.NewRangeByScoreQuery(options.NewScoreBoundary(3, false), -// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). -// +// query := options.NewRangeByScoreQuery( +// options.NewScoreBoundary(3, false), +// options.NewInfiniteScoreBoundary(options.NegativeInfinity)). // SetReverse() // result, err := client.ZRangeWithScores("my_sorted_set", query) // // `result` contains members with scores within the range of negative infinity to 3, in descending order @@ -1894,7 +1993,7 @@ func (client *baseClient) ZRange(key string, rangeQuery options.ZRangeQuery) ([] func (client *baseClient) ZRangeWithScores( key string, rangeQuery options.ZRangeQueryWithScores, -) (map[Result[string]]Result[float64], error) { +) (map[string]float64, error) { args := make([]string, 0, 10) args = append(args, key) args = append(args, rangeQuery.ToArgs()...) @@ -2506,6 +2605,73 @@ func (client *baseClient) XPendingWithOptions( return handleXPendingDetailResponse(result) } +// Creates a new consumer group uniquely identified by `group` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group’s perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// ok, err := client.XGroupCreate("mystream", "mygroup", "0-0") +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreate(key string, group string, id string) (string, error) { + return client.XGroupCreateWithOptions(key, group, id, options.NewXGroupCreateOptions()) +} + +// Creates a new consumer group uniquely identified by `group` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The newly created consumer group name. +// id - Stream entry ID that specifies the last delivered entry in the stream from the new +// group's perspective. The special ID `"$"` can be used to specify the last entry in the stream. +// opts - The options for the command. See [options.XGroupCreateOptions] for details. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// opts := options.NewXGroupCreateOptions().SetMakeStream() +// ok, err := client.XGroupCreateWithOptions("mystream", "mygroup", "0-0", opts) +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupCreateWithOptions( + key string, + group string, + id string, + opts *options.XGroupCreateOptions, +) (string, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group, id}, optionArgs...) + result, err := client.executeCommand(C.XGroupCreate, args) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + func (client *baseClient) Restore(key string, ttl int64, value string) (Result[string], error) { return client.RestoreWithOptions(key, ttl, value, NewRestoreOptionsBuilder()) } @@ -2571,6 +2737,100 @@ func (client *baseClient) Echo(message string) (Result[string], error) { return handleStringOrNilResponse(result) } +// Destroys the consumer group `group` for the stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name to delete. +// +// Return value: +// +// `true` if the consumer group is destroyed. Otherwise, `false`. +// +// Example: +// +// ok, err := client.XGroupDestroy("mystream", "mygroup") +// if !ok || err != nil { +// // handle errors +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-destroy/ +func (client *baseClient) XGroupDestroy(key string, group string) (bool, error) { + result, err := client.executeCommand(C.XGroupDestroy, []string{key, group}) + if err != nil { + return defaultBoolResponse, err + } + return handleBoolResponse(result) +} + +// Sets the last delivered ID for a consumer group. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// id - The stream entry ID that should be set as the last delivered ID for the consumer group. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// ok, err := client.XGroupSetId("mystream", "mygroup", "0-0") +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupSetId(key string, group string, id string) (string, error) { + return client.XGroupSetIdWithOptions(key, group, id, options.NewXGroupSetIdOptionsOptions()) +} + +// Sets the last delivered ID for a consumer group. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// id - The stream entry ID that should be set as the last delivered ID for the consumer group. +// opts - The options for the command. See [options.XGroupSetIdOptions] for details. +// +// Return value: +// +// `"OK"`. +// +// Example: +// +// opts := options.NewXGroupSetIdOptionsOptions().SetEntriesRead(42) +// ok, err := client.XGroupSetIdWithOptions("mystream", "mygroup", "0-0", opts) +// if ok != "OK" || err != nil { +// // handle error +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-create/ +func (client *baseClient) XGroupSetIdWithOptions( + key string, + group string, + id string, + opts *options.XGroupSetIdOptions, +) (string, error) { + optionArgs, _ := opts.ToArgs() + args := append([]string{key, group, id}, optionArgs...) + result, err := client.executeCommand(C.XGroupSetId, args) + if err != nil { + return defaultStringResponse, err + } + return handleStringResponse(result) +} + // Removes all elements in the sorted set stored at `key` with a lexicographical order // between `rangeQuery.Start` and `rangeQuery.End`. // @@ -2580,7 +2840,6 @@ func (client *baseClient) Echo(message string) (Result[string], error) { // // key - The key of the sorted set. // rangeQuery - The range query object representing the minimum and maximum bound of the lexicographical range. -// can be an implementation of [options.LexBoundary]. // // Return value: // @@ -2665,3 +2924,571 @@ 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 handleStringOrNilArrayResponse(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 handleStringOrNilArrayResponse(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 handleStringOrNilArrayResponse(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 handleStringOrNilArrayResponse(result) +} + +func (client *baseClient) SortStore(key string, destination string) (int64, error) { + result, err := client.executeCommand(C.Sort, []string{key, "STORE", destination}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +func (client *baseClient) SortStoreWithOptions( + key string, + destination string, + options *options.SortOptions, +) (int64, error) { + optionArgs := options.ToArgs() + result, err := client.executeCommand(C.Sort, append([]string{key, "STORE", destination}, optionArgs...)) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// XGroupCreateConsumer creates a consumer named `consumer` in the consumer group `group` for the +// stream stored at `key`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The newly created consumer. +// +// Return value: +// +// Returns `true` if the consumer is created. Otherwise, returns `false`. +// +// Example: +// +// //Creates the consumer "myconsumer" in consumer group "mygroup" +// success, err := client.xgroupCreateConsumer("mystream", "mygroup", "myconsumer") +// if err == nil && success { +// fmt.Println("Consumer created") +// } +// +// [valkey.io]: https://valkey.io/commands/xgroup-createconsumer/ +func (client *baseClient) XGroupCreateConsumer( + key string, + group string, + consumer string, +) (bool, error) { + result, err := client.executeCommand(C.XGroupCreateConsumer, []string{key, group, consumer}) + if err != nil { + return false, err + } + return handleBoolResponse(result) +} + +// XGroupDelConsumer deletes a consumer named `consumer` in the consumer group `group`. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The consumer group name. +// consumer - The consumer to delete. +// +// Returns the number of pending messages the `consumer` had before it was deleted. +// +// Example: +// +// // Deletes the consumer "myconsumer" in consumer group "mygroup" +// pendingMsgCount, err := client.XGroupDelConsumer("mystream", "mygroup", "myconsumer") +// if err != nil { +// // handle error +// } +// fmt.Printf("Consumer 'myconsumer' had %d pending messages unclaimed.\n", pendingMsgCount) +// +// [valkey.io]: https://valkey.io/commands/xgroup-delconsumer/ +func (client *baseClient) XGroupDelConsumer( + key string, + group string, + consumer string, +) (int64, error) { + result, err := client.executeCommand(C.XGroupDelConsumer, []string{key, group, consumer}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Returns the number of messages that were successfully acknowledged by the consumer group member +// of a stream. This command should be called on a pending message so that such message does not +// get processed again. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - he consumer group name. +// ids - Stream entry IDs to acknowledge and purge messages. +// +// Return value: +// +// The number of messages that were successfully acknowledged. +// +// Example: +// +// // Assuming streamId1 and streamId2 already exist. +// xackResult, err := client.XAck("key", "groupName", []string{"streamId1", "streamId2"}) +// fmt.Println(xackResult) // 2 +// +// [valkey.io]: https://valkey.io/commands/xack/ +func (client *baseClient) XAck(key string, group string, ids []string) (int64, error) { + result, err := client.executeCommand(C.XAck, append([]string{key, group}, ids...)) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Sets or clears the bit at offset in the string value stored at key. +// The offset is a zero-based index, with `0` being the first element of +// the list, `1` being the next element, and so on. The offset must be +// less than `2^32` and greater than or equal to `0` If a key is +// non-existent then the bit at offset is set to value and the preceding +// bits are set to `0`. +// +// Parameters: +// +// key - The key of the string. +// offset - The index of the bit to be set. +// value - The bit value to set at offset The value must be `0` or `1`. +// +// Return value: +// +// The bit value that was previously stored at offset. +// +// Example: +// +// result, err := client.SetBit("key", 1, 1) +// result: 1 +// +// [valkey.io]: https://valkey.io/commands/setbit/ +func (client *baseClient) SetBit(key string, offset int64, value int64) (int64, error) { + result, err := client.executeCommand(C.SetBit, []string{key, utils.IntToString(offset), utils.IntToString(value)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Returns the bit value at offset in the string value stored at key. +// +// offset should be greater than or equal to zero. +// +// Parameters: +// +// key - The key of the string. +// offset - The index of the bit to return. +// +// Return value: +// The bit at offset of the string. Returns zero if the key is empty or if the positive +// offset exceeds the length of the string. +// +// Example: +// +// result, err := client.GetBit("key1", 1, 1) +// result: 1 +// +// [valkey.io]: https://valkey.io/commands/getbit/ +func (client *baseClient) GetBit(key string, offset int64) (int64, error) { + result, err := client.executeCommand(C.GetBit, []string{key, utils.IntToString(offset)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Wait blocks the current client until all the previous write commands are successfully +// transferred and acknowledged by at least the specified number of replicas or if the timeout is reached, +// whichever is earlier +// +// Parameters: +// +// numberOfReplicas - The number of replicas to reach. +// timeout - The timeout value specified in milliseconds. A value of `0` will +// block indefinitely. +// +// Return value: +// The number of replicas reached by all the writes performed in the context of the current connection. +// +// Example: +// +// result, err := client.Wait(1, 1000) +// if err != nil { +// // handle error +// } +// fmt.Println(result.Value()) // Output: 1 // if cluster has 2 replicasets +// +// [valkey.io]: https://valkey.io/commands/wait/ +func (client *baseClient) Wait(numberOfReplicas int64, timeout int64) (int64, error) { + result, err := client.executeCommand(C.Wait, []string{utils.IntToString(numberOfReplicas), utils.IntToString(timeout)}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Counts the number of set bits (population counting) in a string stored at key. +// +// Parameters: +// +// key - The key for the string to count the set bits of. +// +// Return value: +// The number of set bits in the string. Returns zero if the key is missing as it is +// treated as an empty string. +// +// Example: +// +// result, err := client.BitCount("mykey") +// result: 26 +// +// [valkey.io]: https://valkey.io/commands/bitcount/ +func (client *baseClient) BitCount(key string) (int64, error) { + result, err := client.executeCommand(C.BitCount, []string{key}) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Counts the number of set bits (population counting) in a string stored at key. The +// offsets start and end are zero-based indexes, with `0` being the first element of the +// list, `1` being the next element and so on. These offsets can also be negative numbers +// indicating offsets starting at the end of the list, with `-1` being the last element +// of the list, `-2` being the penultimate, and so on. +// +// Parameters: +// +// key - The key for the string to count the set bits of. +// options - The offset options - see [options.BitOffsetOptions]. +// +// Return value: +// The number of set bits in the string interval specified by start, end, and options. +// Returns zero if the key is missing as it is treated as an empty string. +// +// Example: +// +// opts := NewBitCountOptionsBuilder().SetStart(1).SetEnd(1).SetBitmapIndexType(options.BYTE) +// result, err := client.BitCount("mykey",options) +// result: 6 +// +// [valkey.io]: https://valkey.io/commands/bitcount/ +func (client *baseClient) BitCountWithOptions(key string, opts *options.BitCountOptions) (int64, error) { + optionArgs, err := opts.ToArgs() + if err != nil { + return defaultIntResponse, err + } + commandArgs := append([]string{key}, optionArgs...) + result, err := client.executeCommand(C.BitCount, commandArgs) + if err != nil { + return defaultIntResponse, err + } + return handleIntResponse(result) +} + +// Changes the ownership of a pending message. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The name of the consumer group. +// consumer - The name of the consumer. +// minIdleTime - The minimum idle time in milliseconds. +// ids - The ids of the entries to claim. +// +// Return value: +// +// A `map of message entries with the format `{"entryId": [["entry", "data"], ...], ...}` that were claimed by +// the consumer. +// +// Example: +// +// result, err := client.XClaim("key", "group", "consumer", 1000, []string{"streamId1", "streamId2"}) +// fmt.Println(result) // Output: map[streamId1:[["entry1", "data1"], ["entry2", "data2"]] streamId2:[["entry3", "data3"]]] +// +// [valkey.io]: https://valkey.io/commands/xclaim/ +func (client *baseClient) XClaim( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, +) (map[string][][]string, error) { + return client.XClaimWithOptions(key, group, consumer, minIdleTime, ids, nil) +} + +// Changes the ownership of a pending message. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The name of the consumer group. +// consumer - The name of the consumer. +// minIdleTime - The minimum idle time in milliseconds. +// ids - The ids of the entries to claim. +// options - Stream claim options. +// +// Return value: +// +// A `map` of message entries with the format `{"entryId": [["entry", "data"], ...], ...}` that were claimed by +// the consumer. +// +// Example: +// +// result, err := client.XClaimWithOptions( +// "key", +// "group", +// "consumer", +// 1000, +// []string{"streamId1", "streamId2"}, +// options.NewStreamClaimOptions().SetIdleTime(1), +// ) +// fmt.Println(result) // Output: map[streamId1:[["entry1", "data1"], ["entry2", "data2"]] streamId2:[["entry3", "data3"]]] +// +// [valkey.io]: https://valkey.io/commands/xclaim/ +func (client *baseClient) XClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, + opts *options.StreamClaimOptions, +) (map[string][][]string, error) { + args := append([]string{key, group, consumer, utils.IntToString(minIdleTime)}, ids...) + if opts != nil { + optionArgs, err := opts.ToArgs() + if err != nil { + return nil, err + } + args = append(args, optionArgs...) + } + result, err := client.executeCommand(C.XClaim, args) + if err != nil { + return nil, err + } + return handleMapOfArrayOfStringArrayResponse(result) +} + +// Changes the ownership of a pending message. This function returns an `array` with +// only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The name of the consumer group. +// consumer - The name of the consumer. +// minIdleTime - The minimum idle time in milliseconds. +// ids - The ids of the entries to claim. +// options - Stream claim options. +// +// Return value: +// +// An array of the ids of the entries that were claimed by the consumer. +// +// Example: +// +// result, err := client.XClaimJustId( +// "key", +// "group", +// "consumer", +// 1000, +// []string{"streamId1", "streamId2"}, +// ) +// fmt.Println(result) // Output: ["streamId1", "streamId2"] +// +// [valkey.io]: https://valkey.io/commands/xclaim/ +func (client *baseClient) XClaimJustId( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, +) ([]string, error) { + return client.XClaimJustIdWithOptions(key, group, consumer, minIdleTime, ids, nil) +} + +// Changes the ownership of a pending message. This function returns an `array` with +// only the message/entry IDs, and is equivalent to using `JUSTID` in the Valkey API. +// +// See [valkey.io] for details. +// +// Parameters: +// +// key - The key of the stream. +// group - The name of the consumer group. +// consumer - The name of the consumer. +// minIdleTime - The minimum idle time in milliseconds. +// ids - The ids of the entries to claim. +// options - Stream claim options. +// +// Return value: +// +// An array of the ids of the entries that were claimed by the consumer. +// +// Example: +// +// result, err := client.XClaimJustIdWithOptions( +// "key", +// "group", +// "consumer", +// 1000, +// []string{"streamId1", "streamId2"}, +// options.NewStreamClaimOptions().SetIdleTime(1), +// ) +// fmt.Println(result) // Output: ["streamId1", "streamId2"] +// +// [valkey.io]: https://valkey.io/commands/xclaim/ +func (client *baseClient) XClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, + opts *options.StreamClaimOptions, +) ([]string, error) { + args := append([]string{key, group, consumer, utils.IntToString(minIdleTime)}, ids...) + if opts != nil { + optionArgs, err := opts.ToArgs() + if err != nil { + return nil, err + } + args = append(args, optionArgs...) + } + args = append(args, options.JUST_ID_VALKEY_API) + result, err := client.executeCommand(C.XClaim, args) + if err != nil { + return nil, err + } + return handleStringArrayResponse(result) +} diff --git a/go/api/bitmap_commands.go b/go/api/bitmap_commands.go new file mode 100644 index 0000000000..466df2e6c3 --- /dev/null +++ b/go/api/bitmap_commands.go @@ -0,0 +1,20 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/valkey-io/valkey-glide/go/glide/api/options" + +// Supports commands and transactions for the "Bitmap" group of commands for standalone and cluster clients. +// +// See [valkey.io] for details. +// +// [valkey.io]: https://valkey.io/commands/#bitmap +type BitmapCommands interface { + SetBit(key string, offset int64, value int64) (int64, error) + + GetBit(key string, offset int64) (int64, error) + + BitCount(key string) (int64, error) + + BitCountWithOptions(key string, options *options.BitCountOptions) (int64, error) +} diff --git a/go/api/generic_base_commands.go b/go/api/generic_base_commands.go index 1f15eddd23..1454063040 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. @@ -367,17 +369,17 @@ type GenericBaseCommands interface { // key - string // // Return value: - // If the key exists, the type of the stored value is returned. Otherwise, a none" string is returned. + // If the key exists, the type of the stored value is returned. Otherwise, a "none" string is returned. // // Example: // result, err := client.Type([]string{"key"}) // if err != nil { // // handle error // } - // fmt.Println(result.Value()) // Output: string + // fmt.Println(result) // Output: string // // [valkey.io]: Https://valkey.io/commands/type/ - Type(key string) (Result[string], error) + Type(key string) (string, error) // Renames key to new key. // If new Key already exists it is overwritten. @@ -397,10 +399,10 @@ type GenericBaseCommands interface { // if err != nil { // // handle error // } - // fmt.Println(result.Value()) // Output: OK + // fmt.Println(result) // Output: OK // // [valkey.io]: https://valkey.io/commands/rename/ - Rename(key string, newKey string) (Result[string], error) + Rename(key string, newKey string) (string, error) // Renames key to newkey if newKey does not yet exist. // @@ -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: 1 + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStore(key string, destination string) (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: 1 + // + // [valkey.io]: https://valkey.io/commands/sort/ + SortStoreWithOptions(key string, destination string, sortOptions *options.SortOptions) (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) + + Wait(numberOfReplicas int64, timeout int64) (int64, error) } diff --git a/go/api/glide_client.go b/go/api/glide_client.go index a90575c767..51ea9ef4b4 100644 --- a/go/api/glide_client.go +++ b/go/api/glide_client.go @@ -51,7 +51,7 @@ func (client *glideClient) ConfigSet(parameters map[string]string) (string, erro return handleStringResponse(result) } -func (client *glideClient) ConfigGet(args []string) (map[Result[string]]Result[string], error) { +func (client *glideClient) ConfigGet(args []string) (map[string]string, error) { res, err := client.executeCommand(C.ConfigGet, args) if err != nil { return nil, err diff --git a/go/api/hash_commands.go b/go/api/hash_commands.go index 41e006cc04..0d1eecac6e 100644 --- a/go/api/hash_commands.go +++ b/go/api/hash_commands.go @@ -46,14 +46,10 @@ type HashCommands interface { // // For example: // fieldValueMap, err := client.HGetAll("my_hash") - // // field1 equals api.CreateStringResult("field1") - // // value1 equals api.CreateStringResult("value1") - // // field2 equals api.CreateStringResult("field2") - // // value2 equals api.CreateStringResult("value2") - // // fieldValueMap equals map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} + // // fieldValueMap equals map[string]string{field1: value1, field2: value2} // // [valkey.io]: https://valkey.io/commands/hgetall/ - HGetAll(key string) (map[Result[string]]Result[string], error) + HGetAll(key string) (map[string]string, error) // HMGet returns the values associated with the specified fields in the hash stored at key. // @@ -172,17 +168,14 @@ type HashCommands interface { // key - The key of the hash. // // Return value: - // A slice of Result[string]s containing all the values in the hash, or an empty slice when key does not exist. + // A slice containing all the values in the hash, or an empty slice when key does not exist. // // For example: // values, err := client.HVals("myHash") - // // value1 equals api.CreateStringResult("value1") - // // value2 equals api.CreateStringResult("value2") - // // value3 equals api.CreateStringResult("value3") - // // values equals []api.Result[string]{value1, value2, value3} + // values: []string{"value1", "value2", "value3"} // // [valkey.io]: https://valkey.io/commands/hvals/ - HVals(key string) ([]Result[string], error) + HVals(key string) ([]string, error) // HExists returns if field is an existing field in the hash stored at key. // @@ -215,16 +208,14 @@ type HashCommands interface { // key - The key of the hash. // // Return value: - // A slice of Result[string]s containing all the field names in the hash, or an empty slice when key does not exist. + // A slice containing all the field names in the hash, or an empty slice when key does not exist. // // For example: // names, err := client.HKeys("my_hash") - // // field1 equals api.CreateStringResult("field_1") - // // field2 equals api.CreateStringResult("field_2") - // // names equals []api.Result[string]{field1, field2} + // names: []string{"field1", "field2"} // // [valkey.io]: https://valkey.io/commands/hkeys/ - HKeys(key string) ([]Result[string], error) + HKeys(key string) ([]string, error) // HStrLen returns the string length of the value associated with field in the hash stored at key. // If the key or the field do not exist, 0 is returned. @@ -292,5 +283,11 @@ type HashCommands interface { 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/list_commands.go b/go/api/list_commands.go index 0c64012e6c..1d2942e5ae 100644 --- a/go/api/list_commands.go +++ b/go/api/list_commands.go @@ -71,7 +71,7 @@ type ListCommands interface { // result: nil // // [valkey.io]: https://valkey.io/commands/lpop/ - LPopCount(key string, count int64) ([]Result[string], error) + LPopCount(key string, count int64) ([]string, error) // Returns the index of the first occurrence of element inside the list specified by key. If no match is found, // [api.CreateNilInt64Result()] is returned. @@ -132,13 +132,12 @@ type ListCommands interface { // An array that holds the indices of the matching elements within the list. // // For example: - // result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosCount("my_list", "e", int64(3)) - // result: []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(5), api.CreateInt64Result(6)} - // + // result: []int64{ 4, 5, 6 } // // [valkey.io]: https://valkey.io/commands/lpos/ - LPosCount(key string, element string, count int64) ([]Result[int64], error) + LPosCount(key string, element string, count int64) ([]int64, error) // Returns an array of indices of matching elements within a list based on the given options. If no match is found, an // empty array is returned. @@ -155,21 +154,21 @@ type ListCommands interface { // An array that holds the indices of the matching elements within the list. // // For example: - // 1. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // 1. _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosWithOptions("my_list", "e", int64(1), api.NewLPosOptionsBuilder().SetRank(2)) - // result: []api.Result[int64]{api.CreateInt64Result(5)} - // 2. result, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) + // result: []int64{ 5 } + // 2. _, err := client.RPush("my_list", []string{"a", "b", "c", "d", "e", "e", "e"}) // result, err := client.LPosWithOptions( // "my_list", // "e", // int64(3), // api.NewLPosOptionsBuilder().SetRank(2).SetMaxLen(1000), // ) - // result: []api.Result[int64]{api.CreateInt64Result(5), api.CreateInt64Result(6)} + // result: []int64{ 5, 6 } // // // [valkey.io]: https://valkey.io/commands/lpos/ - LPosCountWithOptions(key string, element string, count int64, options *LPosOptions) ([]Result[int64], error) + LPosCountWithOptions(key string, element string, count int64, options *LPosOptions) ([]int64, error) // Inserts all the specified values at the tail of the list stored at key. // elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. @@ -211,15 +210,14 @@ type ListCommands interface { // // For example: // 1. result, err := client.LRange("my_list", 0, 2) - // result: []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2"), - // api.CreateStringResult("value3")} + // result: []string{ "value1", "value2", "value3" } // 2. result, err := client.LRange("my_list", -2, -1) - // result: []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} + // result: []string{ "value2", "value3" } // 3. result, err := client.LRange("non_existent_key", 0, 2) - // result: []api.Result[string]{} + // result: []string{} // // [valkey.io]: https://valkey.io/commands/lrange/ - LRange(key string, start int64, end int64) ([]Result[string], error) + LRange(key string, start int64, end int64) ([]string, error) // Returns the element at index from the list stored at key. // The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to @@ -357,7 +355,7 @@ type ListCommands interface { // result: nil // // [valkey.io]: https://valkey.io/commands/rpop/ - RPopCount(key string, count int64) ([]Result[string], error) + RPopCount(key string, count int64) ([]string, error) // Inserts element in the list at key either before or after the pivot. // @@ -397,17 +395,17 @@ type ListCommands interface { // timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. // // Return value: - // A two-element array of Result[string] containing the key from which the element was popped and the value of the popped + // A two-element array containing the key from which the element was popped and the value of the popped // element, formatted as [key, value]. - // If no element could be popped and the timeout expired, returns nil. + // If no element could be popped and the timeout expired, returns `nil`. // // For example: // result, err := client.BLPop("list1", "list2", 0.5) - // result: []api.Result[string]{api.CreateStringResult("list1"), api.CreateStringResult("element")} + // result: []string{ "list1", "element" } // // [valkey.io]: https://valkey.io/commands/blpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BLPop(keys []string, timeoutSecs float64) ([]Result[string], error) + BLPop(keys []string, timeoutSecs float64) ([]string, error) // Pops an element from the tail of the first list that is non-empty, with the given keys being checked in the order that // they are given. @@ -424,17 +422,17 @@ type ListCommands interface { // timeoutSecs - The number of seconds to wait for a blocking operation to complete. A value of 0 will block indefinitely. // // Return value: - // A two-element array of Result[string] containing the key from which the element was popped and the value of the popped + // A two-element array containing the key from which the element was popped and the value of the popped // element, formatted as [key, value]. - // If no element could be popped and the timeoutSecs expired, returns nil. + // If no element could be popped and the timeoutSecs expired, returns `nil`. // // For example: // result, err := client.BRPop("list1", "list2", 0.5) - // result: []api.Result[string]{api.CreateStringResult("list1"), api.CreateStringResult("element")} + // result: []string{ "list1", "element" } // // [valkey.io]: https://valkey.io/commands/brpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BRPop(keys []string, timeoutSecs float64) ([]Result[string], error) + BRPop(keys []string, timeoutSecs float64) ([]string, error) // Inserts all the specified values at the tail of the list stored at key, only if key exists and holds a list. If key is // not a list, this performs no operation. @@ -493,10 +491,10 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.LMPop([]string{"my_list"}, api.Left) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/lmpop/ - LMPop(keys []string, listDirection ListDirection) (map[Result[string]][]Result[string], error) + LMPop(keys []string, listDirection ListDirection) (map[string][]string, error) // Pops one or more elements from the first non-empty list from the provided keys. // @@ -516,10 +514,10 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.LMPopCount([]string{"my_list"}, api.Left, int64(1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/lmpop/ - LMPopCount(keys []string, listDirection ListDirection, count int64) (map[Result[string]][]Result[string], error) + LMPopCount(keys []string, listDirection ListDirection, count int64) (map[string][]string, error) // Blocks the connection until it pops one element from the first non-empty list from the provided keys. BLMPop is the // blocking variant of [api.LMPop]. @@ -546,11 +544,11 @@ type ListCommands interface { // For example: // result, err := client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.BLMPop([]string{"my_list"}, api.Left, float64(0.1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/blmpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands - BLMPop(keys []string, listDirection ListDirection, timeoutSecs float64) (map[Result[string]][]Result[string], error) + BLMPop(keys []string, listDirection ListDirection, timeoutSecs float64) (map[string][]string, error) // Blocks the connection until it pops one or more elements from the first non-empty list from the provided keys. // BLMPopCount is the blocking variant of [api.LMPopCount]. @@ -578,7 +576,7 @@ type ListCommands interface { // For example: // result, err: client.LPush("my_list", []string{"one", "two", "three"}) // result, err := client.BLMPopCount([]string{"my_list"}, api.Left, int64(1), float64(0.1)) - // result[api.CreateStringResult("my_list")] = []api.Result[string]{api.CreateStringResult("three")} + // result["my_list"] = []string{"three"} // // [valkey.io]: https://valkey.io/commands/blmpop/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands @@ -587,7 +585,7 @@ type ListCommands interface { listDirection ListDirection, count int64, timeoutSecs float64, - ) (map[Result[string]][]Result[string], error) + ) (map[string][]string, error) // Sets the list element at index to element. // The index is zero-based, so 0 means the first element,1 the second element and so on. Negative indices can be used to @@ -632,9 +630,8 @@ type ListCommands interface { // result.Value(): "one" // updatedList1, err: client.LRange("my_list1", int64(0), int64(-1)) // updatedList2, err: client.LRange("my_list2", int64(0), int64(-1)) - // updatedList1: []api.Result[string]{api.CreateStringResult("two")} - // updatedList2: []api.Result[string]{api.CreateStringResult("one"), api.CreateStringResult("three"), - // api.CreateStringResult("four")} + // updatedList1: []string{ "two" } + // updatedList2: []string{ "one", "three", "four" } // // [valkey.io]: https://valkey.io/commands/lmove/ LMove(source string, destination string, whereFrom ListDirection, whereTo ListDirection) (Result[string], error) @@ -671,9 +668,8 @@ type ListCommands interface { // result.Value(): "one" // updatedList1, err: client.LRange("my_list1", int64(0), int64(-1)) // updatedList2, err: client.LRange("my_list2", int64(0), int64(-1)) - // updatedList1: []api.Result[string]{api.CreateStringResult("two")} - // updatedList2: []api.Result[string]{api.CreateStringResult("one"), api.CreateStringResult("three"), - // api.CreateStringResult("four")} + // updatedList1: []string{ "two" } + // updatedList2: []string{ "one", "three", "four" } // // [valkey.io]: https://valkey.io/commands/blmove/ // [Blocking Commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands diff --git a/go/api/options/bitcount_options.go b/go/api/options/bitcount_options.go new file mode 100644 index 0000000000..db68144c9a --- /dev/null +++ b/go/api/options/bitcount_options.go @@ -0,0 +1,63 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +package options + +import ( + "github.com/valkey-io/valkey-glide/go/glide/utils" +) + +type BitmapIndexType string + +const ( + BYTE BitmapIndexType = "BYTE" + BIT BitmapIndexType = "BIT" +) + +// Optional arguments to `BitCount` in [BitMapCommands] +type BitCountOptions struct { + start *int64 + end *int64 + bitMapIndexType BitmapIndexType +} + +func NewBitCountOptionsBuilder() *BitCountOptions { + return &BitCountOptions{} +} + +// SetStart defines start byte to calculate bitcount in bitcount command. +func (options *BitCountOptions) SetStart(start int64) *BitCountOptions { + options.start = &start + return options +} + +// SetEnd defines start byte to calculate bitcount in bitcount command. +func (options *BitCountOptions) SetEnd(end int64) *BitCountOptions { + options.end = &end + return options +} + +// SetBitmapIndexType to specify start and end are in BYTE or BIT +func (options *BitCountOptions) SetBitmapIndexType(bitMapIndexType BitmapIndexType) *BitCountOptions { + options.bitMapIndexType = bitMapIndexType + return options +} + +// ToArgs converts the options to a list of arguments. +func (opts *BitCountOptions) ToArgs() ([]string, error) { + args := []string{} + var err error + + if opts.start != nil { + args = append(args, utils.IntToString(*opts.start)) + if opts.end != nil { + args = append(args, utils.IntToString(*opts.end)) + if opts.bitMapIndexType != "" { + if opts.bitMapIndexType == BIT || opts.bitMapIndexType == BYTE { + args = append(args, string(opts.bitMapIndexType)) + } + } + } + } + + return args, err +} diff --git a/go/api/options/constants.go b/go/api/options/constants.go index d2d4b594db..1b0e33d540 100644 --- a/go/api/options/constants.go +++ b/go/api/options/constants.go @@ -3,9 +3,10 @@ package options const ( - CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. - MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. - NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. - WithScore string = "WITHSCORE" // Valkey API keyword for the with score option for zrank and zrevrank commands. - NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. + CountKeyword string = "COUNT" // Valkey API keyword used to extract specific number of matching indices from a list. + MatchKeyword string = "MATCH" // Valkey API keyword used to indicate the match filter. + NoValue string = "NOVALUE" // Valkey API keyword for the no value option for hcsan command. + WithScore string = "WITHSCORE" // Valkey API keyword for the with score option for zrank and zrevrank commands. + NoScores string = "NOSCORES" // Valkey API keyword for the no scores option for zscan command. + WithValues string = "WITHVALUES" // Valkey API keyword to query hash values along their names in `HRANDFIELD`. ) 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/options/stream_options.go b/go/api/options/stream_options.go index 4507b0478c..ef2876f6a6 100644 --- a/go/api/options/stream_options.go +++ b/go/api/options/stream_options.go @@ -246,7 +246,6 @@ func (xpo *XPendingOptions) SetConsumer(consumer string) *XPendingOptions { func (xpo *XPendingOptions) ToArgs() ([]string, error) { args := []string{} - // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime if xpo.minIdleTime > 0 { args = append(args, "IDLE") args = append(args, utils.IntToString(xpo.minIdleTime)) @@ -262,3 +261,140 @@ func (xpo *XPendingOptions) ToArgs() ([]string, error) { return args, nil } + +// Optional arguments for `XGroupCreate` in [StreamCommands] +type XGroupCreateOptions struct { + mkStream bool + entriesRead int64 +} + +// Create new empty `XGroupCreateOptions` +func NewXGroupCreateOptions() *XGroupCreateOptions { + return &XGroupCreateOptions{false, -1} +} + +// Once set and if the stream doesn't exist, creates a new stream with a length of `0`. +func (xgco *XGroupCreateOptions) SetMakeStream() *XGroupCreateOptions { + xgco.mkStream = true + return xgco +} + +func (xgco *XGroupCreateOptions) SetEntriesRead(entriesRead int64) *XGroupCreateOptions { + xgco.entriesRead = entriesRead + return xgco +} + +func (xgco *XGroupCreateOptions) ToArgs() ([]string, error) { + var args []string + + // if minIdleTime is set, we need to add an `IDLE` argument along with the minIdleTime + if xgco.mkStream { + args = append(args, "MKSTREAM") + } + + if xgco.entriesRead > -1 { + args = append(args, "ENTRIESREAD", utils.IntToString(xgco.entriesRead)) + } + + return args, nil +} + +// Optional arguments for `XGroupSetId` in [StreamCommands] +type XGroupSetIdOptions struct { + entriesRead int64 +} + +// Create new empty `XGroupSetIdOptions` +func NewXGroupSetIdOptionsOptions() *XGroupSetIdOptions { + return &XGroupSetIdOptions{-1} +} + +// A value representing the number of stream entries already read by the group. +// +// Since Valkey version 7.0.0. +func (xgsio *XGroupSetIdOptions) SetEntriesRead(entriesRead int64) *XGroupSetIdOptions { + xgsio.entriesRead = entriesRead + return xgsio +} + +func (xgsio *XGroupSetIdOptions) ToArgs() ([]string, error) { + var args []string + + if xgsio.entriesRead > -1 { + args = append(args, "ENTRIESREAD", utils.IntToString(xgsio.entriesRead)) + } + + return args, nil +} + +// Optional arguments for `XClaim` in [StreamCommands] +type StreamClaimOptions struct { + idleTime int64 + idleUnixTime int64 + retryCount int64 + isForce bool +} + +func NewStreamClaimOptions() *StreamClaimOptions { + return &StreamClaimOptions{} +} + +// Set the idle time in milliseconds. +func (sco *StreamClaimOptions) SetIdleTime(idleTime int64) *StreamClaimOptions { + sco.idleTime = idleTime + return sco +} + +// Set the idle time in unix-milliseconds. +func (sco *StreamClaimOptions) SetIdleUnixTime(idleUnixTime int64) *StreamClaimOptions { + sco.idleUnixTime = idleUnixTime + return sco +} + +// Set the retry count. +func (sco *StreamClaimOptions) SetRetryCount(retryCount int64) *StreamClaimOptions { + sco.retryCount = retryCount + return sco +} + +// Set the force flag. +func (sco *StreamClaimOptions) SetForce() *StreamClaimOptions { + sco.isForce = true + return sco +} + +// Valkey API keywords for stream claim options +const ( + // ValKey API string to designate IDLE time in milliseconds + IDLE_VALKEY_API string = "IDLE" + // ValKey API string to designate TIME time in unix-milliseconds + TIME_VALKEY_API string = "TIME" + // ValKey API string to designate RETRYCOUNT + RETRY_COUNT_VALKEY_API string = "RETRYCOUNT" + // ValKey API string to designate FORCE + FORCE_VALKEY_API string = "FORCE" + // ValKey API string to designate JUSTID + JUST_ID_VALKEY_API string = "JUSTID" +) + +func (sco *StreamClaimOptions) ToArgs() ([]string, error) { + optionArgs := []string{} + + if sco.idleTime > 0 { + optionArgs = append(optionArgs, IDLE_VALKEY_API, utils.IntToString(sco.idleTime)) + } + + if sco.idleUnixTime > 0 { + optionArgs = append(optionArgs, TIME_VALKEY_API, utils.IntToString(sco.idleUnixTime)) + } + + if sco.retryCount > 0 { + optionArgs = append(optionArgs, RETRY_COUNT_VALKEY_API, utils.IntToString(sco.retryCount)) + } + + if sco.isForce { + optionArgs = append(optionArgs, FORCE_VALKEY_API) + } + + return optionArgs, nil +} diff --git a/go/api/response_handlers.go b/go/api/response_handlers.go index 48a7dc7509..e49c2a5db5 100644 --- a/go/api/response_handlers.go +++ b/go/api/response_handlers.go @@ -168,7 +168,8 @@ func handleStringOrNilResponse(response *C.struct_CommandResponse) (Result[strin return convertCharArrayToString(response, true) } -func convertStringArray(response *C.struct_CommandResponse) ([]Result[string], error) { +// Fix after merging with https://github.com/valkey-io/valkey-glide/pull/2964 +func convertStringOrNilArray(response *C.struct_CommandResponse) ([]Result[string], error) { typeErr := checkResponseType(response, C.Array, false) if typeErr != nil { return nil, typeErr @@ -185,10 +186,31 @@ func convertStringArray(response *C.struct_CommandResponse) ([]Result[string], e return slice, nil } -func handleStringArrayResponse(response *C.struct_CommandResponse) ([]Result[string], error) { +func handle2DStringArrayResponse(response *C.struct_CommandResponse) ([][]string, error) { defer C.free_command_response(response) - - return convertStringArray(response) + typeErr := checkResponseType(response, C.Array, false) + if typeErr != nil { + return nil, typeErr + } + array, err := parseArray(response) + if err != nil { + return nil, err + } + converted, err := arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + false, + }.convert(array) + if err != nil { + return nil, err + } + res, ok := converted.([][]string) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type: %T", converted)} + } + return res, nil } func handleStringArrayOrNullResponse(response *C.struct_CommandResponse) ([]Result[string], error) { @@ -214,6 +236,46 @@ func handleStringArrayOrNullResponse(response *C.struct_CommandResponse) ([]Resu return slice, nil } +// array could be nillable, but strings - aren't +func convertStringArray(response *C.struct_CommandResponse, isNilable bool) ([]string, error) { + typeErr := checkResponseType(response, C.Array, isNilable) + if typeErr != nil { + return nil, typeErr + } + + if isNilable && response.array_value == nil { + return nil, nil + } + + slice := make([]string, 0, response.array_value_len) + for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { + res, err := convertCharArrayToString(&v, false) + if err != nil { + return nil, err + } + slice = append(slice, res.Value()) + } + return slice, nil +} + +func handleStringOrNilArrayResponse(response *C.struct_CommandResponse) ([]Result[string], error) { + defer C.free_command_response(response) + + return convertStringOrNilArray(response) +} + +func handleStringArrayResponse(response *C.struct_CommandResponse) ([]string, error) { + defer C.free_command_response(response) + + return convertStringArray(response, false) +} + +func handleStringArrayOrNilResponse(response *C.struct_CommandResponse) ([]string, error) { + defer C.free_command_response(response) + + return convertStringArray(response, true) +} + func handleIntResponse(response *C.struct_CommandResponse) (int64, error) { defer C.free_command_response(response) @@ -240,7 +302,7 @@ func handleIntOrNilResponse(response *C.struct_CommandResponse) (Result[int64], return CreateInt64Result(int64(response.int_value)), nil } -func handleIntArrayResponse(response *C.struct_CommandResponse) ([]Result[int64], error) { +func handleIntArrayResponse(response *C.struct_CommandResponse) ([]int64, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Array, false) @@ -248,13 +310,13 @@ func handleIntArrayResponse(response *C.struct_CommandResponse) ([]Result[int64] return nil, typeErr } - slice := make([]Result[int64], 0, response.array_value_len) + slice := make([]int64, 0, response.array_value_len) for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { err := checkResponseType(&v, C.Int, false) if err != nil { return nil, err } - slice = append(slice, CreateInt64Result(int64(v.int_value))) + slice = append(slice, int64(v.int_value)) } return slice, nil } @@ -339,7 +401,7 @@ func handleBoolArrayResponse(response *C.struct_CommandResponse) ([]bool, error) return slice, nil } -func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[Result[string]]Result[float64], error) { +func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[string]float64, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, false) @@ -347,23 +409,26 @@ func handleStringDoubleMapResponse(response *C.struct_CommandResponse) (map[Resu return nil, typeErr } - m := make(map[Result[string]]Result[float64], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - typeErr := checkResponseType(v.map_value, C.Float, false) - if typeErr != nil { - return nil, typeErr - } - value := CreateFloat64Result(float64(v.map_value.float_value)) - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err } - return m, nil + aMap := data.(map[string]interface{}) + + converted, err := mapConverter[float64]{ + nil, false, + }.convert(aMap) + if err != nil { + return nil, err + } + result, ok := converted.(map[string]float64) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type of map: %T", converted)} + } + return result, nil } -func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[Result[string]]Result[string], error) { +func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[string]string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, false) @@ -371,25 +436,28 @@ func handleStringToStringMapResponse(response *C.struct_CommandResponse) (map[Re return nil, typeErr } - m := make(map[Result[string]]Result[string], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - value, err := convertCharArrayToString(v.map_value, true) - if err != nil { - return nil, err - } - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err } + aMap := data.(map[string]interface{}) - return m, nil + converted, err := mapConverter[string]{ + nil, false, + }.convert(aMap) + if err != nil { + return nil, err + } + result, ok := converted.(map[string]string) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type of map: %T", converted)} + } + return result, nil } -func handleStringToStringArrayMapOrNullResponse( +func handleStringToStringArrayMapOrNilResponse( response *C.struct_CommandResponse, -) (map[Result[string]][]Result[string], error) { +) (map[string][]string, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Map, true) @@ -401,23 +469,28 @@ func handleStringToStringArrayMapOrNullResponse( return nil, nil } - m := make(map[Result[string]][]Result[string], response.array_value_len) - for _, v := range unsafe.Slice(response.array_value, response.array_value_len) { - key, err := convertCharArrayToString(v.map_key, true) - if err != nil { - return nil, err - } - value, err := convertStringArray(v.map_value) - if err != nil { - return nil, err - } - m[key] = value + data, err := parseMap(response) + if err != nil { + return nil, err + } + + converters := mapConverter[[]string]{ + arrayConverter[string]{}, + false, + } + + res, err := converters.convert(data) + if err != nil { + return nil, err + } + if result, ok := res.(map[string][]string); ok { + return result, nil } - return m, nil + return nil, &RequestError{fmt.Sprintf("unexpected type received: %T", res)} } -func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[string]]struct{}, error) { +func handleStringSetResponse(response *C.struct_CommandResponse) (map[string]struct{}, error) { defer C.free_command_response(response) typeErr := checkResponseType(response, C.Sets, false) @@ -425,13 +498,13 @@ func handleStringSetResponse(response *C.struct_CommandResponse) (map[Result[str return nil, typeErr } - slice := make(map[Result[string]]struct{}, response.sets_value_len) + slice := make(map[string]struct{}, response.sets_value_len) for _, v := range unsafe.Slice(response.sets_value, response.sets_value_len) { res, err := convertCharArrayToString(&v, true) if err != nil { return nil, err } - slice[res] = struct{}{} + slice[res.Value()] = struct{}{} } return slice, nil @@ -515,6 +588,7 @@ type mapConverter[T any] struct { canBeNil bool } +// Converts an untyped map into a map[string]T func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { if data == nil { if node.canBeNil { @@ -525,14 +599,17 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { } result := make(map[string]T) + // Iterate over the map and convert each value to T for key, value := range data.(map[string]interface{}) { if node.next == nil { + // try direct conversion to T when there is no next converter valueT, ok := value.(T) if !ok { return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", value, getType[T]())} } result[key] = valueT } else { + // nested iteration when there is a next converter val, err := node.next.convert(value) if err != nil { return nil, err @@ -542,6 +619,7 @@ func (node mapConverter[T]) convert(data interface{}) (interface{}, error) { result[key] = null continue } + // convert to T valueT, ok := val.(T) if !ok { return nil, &RequestError{fmt.Sprintf("Unexpected type of map element: %T, expected: %v", val, getType[T]())} @@ -601,6 +679,38 @@ func (node arrayConverter[T]) convert(data interface{}) (interface{}, error) { // TODO: convert sets +func handleMapOfArrayOfStringArrayResponse(response *C.struct_CommandResponse) (map[string][][]string, error) { + defer C.free_command_response(response) + + typeErr := checkResponseType(response, C.Map, false) + if typeErr != nil { + return nil, typeErr + } + mapData, err := parseMap(response) + if err != nil { + return nil, err + } + converted, err := mapConverter[[][]string]{ + arrayConverter[[]string]{ + arrayConverter[string]{ + nil, + false, + }, + false, + }, + false, + }.convert(mapData) + if err != nil { + return nil, err + } + claimedEntries, ok := converted.(map[string][][]string) + if !ok { + return nil, &RequestError{fmt.Sprintf("unexpected type of second element: %T", converted)} + } + + return claimedEntries, nil +} + func handleXAutoClaimResponse(response *C.struct_CommandResponse) (XAutoClaimResponse, error) { defer C.free_command_response(response) var null XAutoClaimResponse // default response diff --git a/go/api/server_management_commands.go b/go/api/server_management_commands.go index 37954f543a..3653f17903 100644 --- a/go/api/server_management_commands.go +++ b/go/api/server_management_commands.go @@ -37,11 +37,11 @@ type ServerManagementCommands interface { // // For example: // result, err := client.ConfigGet([]string{"timeout" , "maxmemory"}) - // result[api.CreateStringResult("timeout")] = api.CreateStringResult("1000") - // result[api.CreateStringResult"maxmemory")] = api.CreateStringResult("1GB") + // // result["timeout"] = "1000" + // // result["maxmemory"] = "1GB" // // [valkey.io]: https://valkey.io/commands/config-get/ - ConfigGet(args []string) (map[Result[string]]Result[string], error) + ConfigGet(args []string) (map[string]string, error) // Sets configuration parameters to the specified values. // diff --git a/go/api/set_commands.go b/go/api/set_commands.go index 5d2315ae74..bed1d65200 100644 --- a/go/api/set_commands.go +++ b/go/api/set_commands.go @@ -54,20 +54,16 @@ type SetCommands interface { // key - The key from which to retrieve the set members. // // Return value: - // A map[Result[string]]struct{} containing all members of the set. - // Returns an empty map if key does not exist. + // A `map[string]struct{}` containing all members of the set. + // Returns an empty collection if key does not exist. // // For example: // // Assume set "my_set" contains: "member1", "member2" // result, err := client.SMembers("my_set") - // // result equals: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("member1"): {}, - // // api.CreateStringResult("member2"): {} - // // } + // // result: map[string]struct{}{ "member1": {}, "member2": {} } // // [valkey.io]: https://valkey.io/commands/smembers/ - SMembers(key string) (map[Result[string]]struct{}, error) + SMembers(key string) (map[string]struct{}, error) // SCard retrieves the set cardinality (number of elements) of the set stored at key. // @@ -119,19 +115,16 @@ type SetCommands interface { // keys - The keys of the sets to diff. // // Return value: - // A map[Result[string]]struct{} representing the difference between the sets. + // A `map[string]struct{}` representing the difference between the sets. // If a key does not exist, it is treated as an empty set. // // Example: // result, err := client.SDiff([]string{"set1", "set2"}) - // // result might contain: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("element"): {}, - // // } + // // result: map[string]struct{}{ "element": {} } // // Indicates that "element" is present in "set1", but missing in "set2" // // [valkey.io]: https://valkey.io/commands/sdiff/ - SDiff(keys []string) (map[Result[string]]struct{}, error) + SDiff(keys []string) (map[string]struct{}, error) // SDiffStore stores the difference between the first set and all the successive sets in keys // into a new set at destination. @@ -165,20 +158,16 @@ type SetCommands interface { // keys - The keys of the sets to intersect. // // Return value: - // A map[Result[string]]struct{} containing members which are present in all given sets. - // If one or more sets do not exist, an empty map will be returned. - // + // A `map[string]struct{}` containing members which are present in all given sets. + // If one or more sets do not exist, an empty collection will be returned. // // Example: // result, err := client.SInter([]string{"set1", "set2"}) - // // result might contain: - // // map[Result[string]]struct{}{ - // // api.CreateStringResult("element"): {}, - // // } + // // result: map[string]struct{}{ "element": {} } // // Indicates that "element" is present in both "set1" and "set2" // // [valkey.io]: https://valkey.io/commands/sinter/ - SInter(keys []string) (map[Result[string]]struct{}, error) + SInter(keys []string) (map[string]struct{}, error) // Stores the members of the intersection of all given sets specified by `keys` into a new set at `destination` // @@ -353,9 +342,8 @@ type SetCommands interface { // keys - The keys of the sets. // // Return value: - // A map[Result[string]]struct{} of members which are present in at least one of the given sets. - // If none of the sets exist, an empty map will be returned. - // + // A `map[string]struct{}` of members which are present in at least one of the given sets. + // If none of the sets exist, an empty collection will be returned. // // Example: // result1, err := client.SAdd("my_set1", []string {"member1", "member2"}) @@ -367,15 +355,15 @@ type SetCommands interface { // // result.IsNil(): false // // result3, err := client.SUnion([]string {"my_set1", "my_set2"}) - // // result3.Value(): "{'member1', 'member2', 'member3'}" + // // result3: "{'member1', 'member2', 'member3'}" // // err: nil // // result4, err := client.SUnion([]string {"my_set1", "non_existing_set"}) - // // result4.Value(): "{'member1', 'member2'}" + // // result4: "{'member1', 'member2'}" // // err: nil // // [valkey.io]: https://valkey.io/commands/sunion/ - SUnion(keys []string) (map[Result[string]]struct{}, error) + SUnion(keys []string) (map[string]struct{}, error) SScan(key string, cursor string) (string, []string, error) diff --git a/go/api/sorted_set_commands.go b/go/api/sorted_set_commands.go index 47b505a558..62d06091bd 100644 --- a/go/api/sorted_set_commands.go +++ b/go/api/sorted_set_commands.go @@ -127,10 +127,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.zpopmin("mySortedSet") - // fmt.Println(res.Value()) // Output: map["member1":5.0] + // fmt.Println(res) // Output: map["member1": 5.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMin(key string) (map[Result[string]]Result[float64], error) + ZPopMin(key string) (map[string]float64, error) // Removes and returns up to `count` members with the lowest scores from the sorted set // stored at the specified `key`. @@ -148,10 +148,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.ZPopMinWithCount("mySortedSet", 2) - // fmt.Println(res.Value()) // Output: map["member1":5.0, "member2":6.0] + // fmt.Println(res) // Output: map["member1": 5.0, "member2": 6.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMinWithCount(key string, count int64) (map[Result[string]]Result[float64], error) + ZPopMinWithCount(key string, count int64) (map[string]float64, error) // Removes and returns the member with the highest score from the sorted set stored at the // specified `key`. @@ -168,10 +168,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.zpopmax("mySortedSet") - // fmt.Println(res.Value()) // Output: map["member2":8.0] + // fmt.Println(res) // Output: map["member2": 8.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMax(key string) (map[Result[string]]Result[float64], error) + ZPopMax(key string) (map[string]float64, error) // Removes and returns up to `count` members with the highest scores from the sorted set // stored at the specified `key`. @@ -189,10 +189,10 @@ type SortedSetCommands interface { // // Example: // res, err := client.ZPopMaxWithCount("mySortedSet", 2) - // fmt.Println(res.Value()) // Output: map["member1":5.0, "member2":6.0] + // fmt.Println(res) // Output: map["member1": 5.0, "member2": 6.0] // // [valkey.io]: https://valkey.io/commands/zpopmin/ - ZPopMaxWithCount(key string, count int64) (map[Result[string]]Result[float64], error) + ZPopMaxWithCount(key string, count int64) (map[string]float64, error) // Removes the specified members from the sorted set stored at `key`. // Specified members that are not a member of this set are ignored. @@ -264,9 +264,9 @@ type SortedSetCommands interface { // [blocking commands]: https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#blocking-commands BZPopMin(keys []string, timeoutSecs float64) (Result[KeyWithMemberAndScore], error) - ZRange(key string, rangeQuery options.ZRangeQuery) ([]Result[string], error) + ZRange(key string, rangeQuery options.ZRangeQuery) ([]string, error) - ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[Result[string]]Result[float64], error) + ZRangeWithScores(key string, rangeQuery options.ZRangeQueryWithScores) (map[string]float64, error) // Returns the rank of `member` in the sorted set stored at `key`, with // scores ordered from low to high, starting from `0`. diff --git a/go/api/stream_commands.go b/go/api/stream_commands.go index 5005c47373..40ae0fb520 100644 --- a/go/api/stream_commands.go +++ b/go/api/stream_commands.go @@ -148,4 +148,48 @@ type StreamCommands interface { XPending(key string, group string) (XPendingSummary, error) XPendingWithOptions(key string, group string, options *options.XPendingOptions) ([]XPendingDetail, error) + + XGroupSetId(key string, group string, id string) (string, error) + + XGroupSetIdWithOptions(key string, group string, id string, opts *options.XGroupSetIdOptions) (string, error) + + XGroupCreate(key string, group string, id string) (string, error) + + XGroupCreateWithOptions(key string, group string, id string, opts *options.XGroupCreateOptions) (string, error) + + XGroupDestroy(key string, group string) (bool, error) + + XGroupCreateConsumer(key string, group string, consumer string) (bool, error) + + XGroupDelConsumer(key string, group string, consumer string) (int64, error) + + XAck(key string, group string, ids []string) (int64, error) + + XClaim( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, + ) (map[string][][]string, error) + + XClaimWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, + options *options.StreamClaimOptions, + ) (map[string][][]string, error) + + XClaimJustId(key string, group string, consumer string, minIdleTime int64, ids []string) ([]string, error) + + XClaimJustIdWithOptions( + key string, + group string, + consumer string, + minIdleTime int64, + ids []string, + options *options.StreamClaimOptions, + ) ([]string, error) } diff --git a/go/integTest/shared_commands_test.go b/go/integTest/shared_commands_test.go index 184cf3e430..02fc2fc4c2 100644 --- a/go/integTest/shared_commands_test.go +++ b/go/integTest/shared_commands_test.go @@ -3,6 +3,7 @@ package integTest import ( + "fmt" "math" "reflect" "strconv" @@ -635,9 +636,14 @@ func (suite *GlideTestSuite) TestHSet_WithExistingKey() { func (suite *GlideTestSuite) TestHSet_byteString() { suite.runWithDefaultClients(func(client api.BaseClient) { + field1 := string([]byte{0xFF, 0x00, 0xAA}) + value1 := string([]byte{0xDE, 0xAD, 0xBE, 0xEF}) + field2 := string([]byte{0x01, 0x02, 0x03, 0xFE}) + value2 := string([]byte{0xCA, 0xFE, 0xBA, 0xBE}) + fields := map[string]string{ - string([]byte{0xFF, 0x00, 0xAA}): string([]byte{0xDE, 0xAD, 0xBE, 0xEF}), - string([]byte{0x01, 0x02, 0x03, 0xFE}): string([]byte{0xCA, 0xFE, 0xBA, 0xBE}), + field1: value1, + field2: value2, } key := string([]byte{0x01, 0x02, 0x03, 0xFE}) @@ -646,16 +652,8 @@ func (suite *GlideTestSuite) TestHSet_byteString() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HGetAll(key) - key1 := api.CreateStringResult(string([]byte{0xFF, 0x00, 0xAA})) - value1 := api.CreateStringResult(string([]byte{0xDE, 0xAD, 0xBE, 0xEF})) - key2 := api.CreateStringResult(string([]byte{0x01, 0x02, 0x03, 0xFE})) - value2 := api.CreateStringResult(string([]byte{0xCA, 0xFE, 0xBA, 0xBE})) - fieldsResult := map[api.Result[string]]api.Result[string]{ - key1: value1, - key2: value2, - } assert.Nil(suite.T(), err) - assert.Equal(suite.T(), fieldsResult, res2) + assert.Equal(suite.T(), fields, res2) }) } @@ -728,14 +726,9 @@ func (suite *GlideTestSuite) TestHGetAll_WithExistingKey() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(2), res1) - field1 := api.CreateStringResult("field1") - value1 := api.CreateStringResult("value1") - field2 := api.CreateStringResult("field2") - value2 := api.CreateStringResult("value2") - fieldsResult := map[api.Result[string]]api.Result[string]{field1: value1, field2: value2} res2, err := client.HGetAll(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), fieldsResult, res2) + assert.Equal(suite.T(), fields, res2) }) } @@ -818,10 +811,8 @@ func (suite *GlideTestSuite) TestHSetNX_WithNotExistingKey() { assert.True(suite.T(), res1) res2, err := client.HGetAll(key) - field1 := api.CreateStringResult("field1") - value1 := api.CreateStringResult("value1") assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[api.Result[string]]api.Result[string]{field1: value1}, res2) + assert.Equal(suite.T(), map[string]string{"field1": "value1"}, res2) }) } @@ -922,11 +913,8 @@ func (suite *GlideTestSuite) TestHVals_WithExistingKey() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HVals(key) - value1 := api.CreateStringResult("value1") - value2 := api.CreateStringResult("value2") assert.Nil(suite.T(), err) - assert.Contains(suite.T(), res2, value1) - assert.Contains(suite.T(), res2, value2) + assert.ElementsMatch(suite.T(), []string{"value1", "value2"}, res2) }) } @@ -936,7 +924,7 @@ func (suite *GlideTestSuite) TestHVals_WithNotExistingKey() { res, err := client.HVals(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res) + assert.Empty(suite.T(), res) }) } @@ -990,11 +978,8 @@ func (suite *GlideTestSuite) TestHKeys_WithExistingKey() { assert.Equal(suite.T(), int64(2), res1) res2, err := client.HKeys(key) - field1 := api.CreateStringResult("field1") - field2 := api.CreateStringResult("field2") assert.Nil(suite.T(), err) - assert.Contains(suite.T(), res2, field1) - assert.Contains(suite.T(), res2, field2) + assert.ElementsMatch(suite.T(), []string{"field1", "field2"}, res2) }) } @@ -1004,7 +989,7 @@ func (suite *GlideTestSuite) TestHKeys_WithNotExistingKey() { res, err := client.HKeys(key) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res) + assert.Empty(suite.T(), res) }) } @@ -1282,6 +1267,77 @@ func (suite *GlideTestSuite) TestHScan() { }) } +func (suite *GlideTestSuite) TestHRandField() { + suite.SkipIfServerVersionLowerThanBy("6.2.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + + // key does not exist + res, err := client.HRandField(key) + assert.NoError(suite.T(), err) + assert.True(suite.T(), res.IsNil()) + resc, err := client.HRandFieldWithCount(key, 5) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), resc) + rescv, err := client.HRandFieldWithCountWithValues(key, 5) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), rescv) + + data := map[string]string{"f1": "v1", "f2": "v2", "f3": "v3"} + hset, err := client.HSet(key, data) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), hset) + + fields := make([]string, 0, len(data)) + for k := range data { + fields = append(fields, k) + } + res, err = client.HRandField(key) + assert.NoError(suite.T(), err) + assert.Contains(suite.T(), fields, res.Value()) + + // With Count - positive count + resc, err = client.HRandFieldWithCount(key, 5) + assert.NoError(suite.T(), err) + assert.ElementsMatch(suite.T(), fields, resc) + + // With Count - negative count + resc, err = client.HRandFieldWithCount(key, -5) + assert.NoError(suite.T(), err) + assert.Len(suite.T(), resc, 5) + for _, field := range resc { + assert.Contains(suite.T(), fields, field) + } + + // With values - positive count + rescv, err = client.HRandFieldWithCountWithValues(key, 5) + assert.NoError(suite.T(), err) + resvMap := make(map[string]string) + for _, pair := range rescv { + resvMap[pair[0]] = pair[1] + } + assert.Equal(suite.T(), data, resvMap) + + // With values - negative count + rescv, err = client.HRandFieldWithCountWithValues(key, -5) + assert.NoError(suite.T(), err) + assert.Len(suite.T(), resc, 5) + for _, pair := range rescv { + assert.Contains(suite.T(), fields, pair[0]) + } + + // key exists but holds non hash type value + key = uuid.NewString() + suite.verifyOK(client.Set(key, "HRandField")) + _, err = client.HRandField(key) + assert.IsType(suite.T(), &api.RequestError{}, err) + _, err = client.HRandFieldWithCount(key, 42) + assert.IsType(suite.T(), &api.RequestError{}, err) + _, err = client.HRandFieldWithCountWithValues(key, 42) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { suite.runWithDefaultClients(func(client api.BaseClient) { list := []string{"value4", "value3", "value2", "value1"} @@ -1295,10 +1351,9 @@ func (suite *GlideTestSuite) TestLPushLPop_WithExistingKey() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), "value1", res2.Value()) - resultList := []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value3")} res3, err := client.LPopCount(key, 2) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, res3) + assert.Equal(suite.T(), []string{"value2", "value3"}, res3) }) } @@ -1312,7 +1367,7 @@ func (suite *GlideTestSuite) TestLPop_nonExistingKey() { res2, err := client.LPopCount(key, 2) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) }) } @@ -1327,7 +1382,7 @@ func (suite *GlideTestSuite) TestLPushLPop_typeError() { assert.IsType(suite.T(), &api.RequestError{}, err) res2, err := client.LPopCount(key, 2) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -1412,33 +1467,29 @@ func (suite *GlideTestSuite) TestLPosCount() { assert.Nil(suite.T(), err) res2, err := client.LPosCount(key, "a", int64(2)) - assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1)}, res2) + assert.Equal(suite.T(), []int64{0, 1}, res2) assert.Nil(suite.T(), err) res3, err := client.LPosCount(key, "a", int64(0)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, - res3, - ) + assert.Equal(suite.T(), []int64{0, 1, 4}, res3) assert.Nil(suite.T(), err) // invalid count value res4, err := client.LPosCount(key, "a", int64(-1)) - assert.Equal(suite.T(), ([]api.Result[int64])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) // non-existent key res5, err := client.LPosCount("non_existent_key", "a", int64(1)) - assert.Equal(suite.T(), []api.Result[int64]{}, res5) + assert.Empty(suite.T(), res5) assert.Nil(suite.T(), err) // wrong key data type keyString := uuid.NewString() suite.verifyOK(client.Set(keyString, "value")) res6, err := client.LPosCount(keyString, "a", int64(1)) - assert.Equal(suite.T(), ([]api.Result[int64])(nil), res6) + assert.Nil(suite.T(), res6) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -1453,24 +1504,16 @@ func (suite *GlideTestSuite) TestLPosCount_withOptions() { assert.Nil(suite.T(), err) res2, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(1)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(0), api.CreateInt64Result(1), api.CreateInt64Result(4)}, - res2, - ) + assert.Equal(suite.T(), []int64{0, 1, 4}, res2) assert.Nil(suite.T(), err) res3, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(2)) - assert.Equal(suite.T(), []api.Result[int64]{api.CreateInt64Result(1), api.CreateInt64Result(4)}, res3) + assert.Equal(suite.T(), []int64{1, 4}, res3) assert.Nil(suite.T(), err) // reverse traversal res4, err := client.LPosCountWithOptions(key, "a", int64(0), api.NewLPosOptionsBuilder().SetRank(-1)) - assert.Equal( - suite.T(), - []api.Result[int64]{api.CreateInt64Result(4), api.CreateInt64Result(1), api.CreateInt64Result(0)}, - res4, - ) + assert.Equal(suite.T(), []int64{4, 1, 0}, res4) assert.Nil(suite.T(), err) }) } @@ -1587,21 +1630,21 @@ func (suite *GlideTestSuite) TestSUnionStore() { memberArray1 := []string{"a", "b", "c"} memberArray2 := []string{"c", "d", "e"} memberArray3 := []string{"e", "f", "g"} - expected1 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, + expected1 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, } - expected2 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, - api.CreateStringResult("f"): {}, - api.CreateStringResult("g"): {}, + expected2 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, + "f": {}, + "g": {}, } t := suite.T() @@ -1790,9 +1833,7 @@ func (suite *GlideTestSuite) TestSDiff() { result, err := client.SDiff([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), result, 2) - assert.Contains(suite.T(), result, api.CreateStringResult("a")) - assert.Contains(suite.T(), result, api.CreateStringResult("b")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}}, result) }) } @@ -1818,10 +1859,7 @@ func (suite *GlideTestSuite) TestSDiff_WithSingleKeyExist() { res2, err := client.SDiff([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 3) - assert.Contains(suite.T(), res2, api.CreateStringResult("a")) - assert.Contains(suite.T(), res2, api.CreateStringResult("b")) - assert.Contains(suite.T(), res2, api.CreateStringResult("c")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}, "c": {}}, res2) }) } @@ -1845,9 +1883,7 @@ func (suite *GlideTestSuite) TestSDiffStore() { members, err := client.SMembers(key3) assert.Nil(suite.T(), err) - assert.Len(suite.T(), members, 2) - assert.Contains(suite.T(), members, api.CreateStringResult("a")) - assert.Contains(suite.T(), members, api.CreateStringResult("b")) + assert.Equal(suite.T(), map[string]struct{}{"a": {}, "b": {}}, members) }) } @@ -1882,9 +1918,7 @@ func (suite *GlideTestSuite) TestSinter() { members, err := client.SInter([]string{key1, key2}) assert.Nil(suite.T(), err) - assert.Len(suite.T(), members, 2) - assert.Contains(suite.T(), members, api.CreateStringResult("c")) - assert.Contains(suite.T(), members, api.CreateStringResult("d")) + assert.Equal(suite.T(), map[string]struct{}{"c": {}, "d": {}}, members) }) } @@ -1925,10 +1959,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res4, err := client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res4, 1) - for key := range res4 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res4) // overwrite existing set, which is also a source set res5, err := client.SInterStore(key2, []string{key1, key2}) @@ -1937,10 +1968,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res6, err := client.SMembers(key2) assert.NoError(t, err) - assert.Len(t, res6, 1) - for key := range res6 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res6) // source set is the same as the existing set res7, err := client.SInterStore(key1, []string{key2}) @@ -1949,10 +1977,7 @@ func (suite *GlideTestSuite) TestSinterStore() { res8, err := client.SMembers(key2) assert.NoError(t, err) - assert.Len(t, res8, 1) - for key := range res8 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res8) // intersection with non-existing key res9, err := client.SInterStore(key1, []string{key2, nonExistingKey}) @@ -1987,10 +2012,7 @@ func (suite *GlideTestSuite) TestSinterStore() { // check that the key is now empty res13, err := client.SMembers(stringKey) assert.NoError(t, err) - assert.Len(t, res13, 1) - for key := range res13 { - assert.Equal(t, key.Value(), "c") - } + assert.Equal(t, map[string]struct{}{"c": {}}, res13) }) } @@ -2138,17 +2160,17 @@ func (suite *GlideTestSuite) TestSUnion() { nonSetKey := uuid.NewString() memberList1 := []string{"a", "b", "c"} memberList2 := []string{"b", "c", "d", "e"} - expected1 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, - api.CreateStringResult("d"): {}, - api.CreateStringResult("e"): {}, + expected1 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, } - expected2 := map[api.Result[string]]struct{}{ - api.CreateStringResult("a"): {}, - api.CreateStringResult("b"): {}, - api.CreateStringResult("c"): {}, + expected2 := map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, } res1, err := client.SAdd(key1, memberList1) @@ -2165,7 +2187,7 @@ func (suite *GlideTestSuite) TestSUnion() { res4, err := client.SUnion([]string{key3}) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), map[api.Result[string]]struct{}{}, res4) + assert.Empty(suite.T(), res4) res5, err := client.SUnion([]string{key1, key3}) assert.Nil(suite.T(), err) @@ -2210,20 +2232,11 @@ func (suite *GlideTestSuite) TestSMove() { res4, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet := map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res4)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res4) res5, err := client.SMembers(key2) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("1"): {}, - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res5)) + assert.Equal(suite.T(), map[string]struct{}{"1": {}, "2": {}, "3": {}}, res5) // moved element already exists in the destination set res6, err := client.SMove(key2, key1, "2") @@ -2232,19 +2245,11 @@ func (suite *GlideTestSuite) TestSMove() { res7, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res7)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res7) res8, err := client.SMembers(key2) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("1"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res8)) + assert.Equal(suite.T(), map[string]struct{}{"1": {}, "3": {}}, res8) // attempt to move from a non-existing key res9, err := client.SMove(nonExistingKey, key1, "4") @@ -2253,11 +2258,7 @@ func (suite *GlideTestSuite) TestSMove() { res10, err := client.SMembers(key1) assert.NoError(t, err) - expectedSet = map[api.Result[string]]struct{}{ - api.CreateStringResult("2"): {}, - api.CreateStringResult("3"): {}, - } - assert.True(t, reflect.DeepEqual(expectedSet, res10)) + assert.Equal(suite.T(), map[string]struct{}{"2": {}, "3": {}}, res10) // move to a new set res11, err := client.SMove(key1, key3, "2") @@ -2266,13 +2267,11 @@ func (suite *GlideTestSuite) TestSMove() { res12, err := client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) res13, err := client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res13, 1) - assert.Contains(t, res13, api.CreateStringResult("2")) + assert.Equal(suite.T(), map[string]struct{}{"2": {}}, res13) // attempt to move a missing element res14, err := client.SMove(key1, key3, "42") @@ -2281,13 +2280,11 @@ func (suite *GlideTestSuite) TestSMove() { res12, err = client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) res13, err = client.SMembers(key3) assert.NoError(t, err) - assert.Len(t, res13, 1) - assert.Contains(t, res13, api.CreateStringResult("2")) + assert.Equal(suite.T(), map[string]struct{}{"2": {}}, res13) // moving missing element to missing key res15, err := client.SMove(key1, nonExistingKey, "42") @@ -2296,8 +2293,7 @@ func (suite *GlideTestSuite) TestSMove() { res12, err = client.SMembers(key1) assert.NoError(t, err) - assert.Len(t, res12, 1) - assert.Contains(t, res12, api.CreateStringResult("3")) + assert.Equal(suite.T(), map[string]struct{}{"3": {}}, res12) // key exists but is not contain a set _, err = client.Set(stringKey, "value") @@ -2423,25 +2419,19 @@ func (suite *GlideTestSuite) TestLRange() { assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(4), res1) - resultList := []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value4"), - } res2, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), resultList, res2) + assert.Equal(suite.T(), []string{"value1", "value2", "value3", "value4"}, res2) res3, err := client.LRange("non_existing_key", int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res3) + assert.Empty(suite.T(), res3) key2 := uuid.NewString() suite.verifyOK(client.Set(key2, "value")) res4, err := client.LRange(key2, int64(0), int64(1)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2493,13 +2483,13 @@ func (suite *GlideTestSuite) TestLTrim() { res2, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1"), api.CreateStringResult("value2")}, res2) + assert.Equal(suite.T(), []string{"value1", "value2"}, res2) suite.verifyOK(client.LTrim(key, int64(4), int64(2))) res3, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res3) + assert.Empty(suite.T(), res3) key2 := uuid.NewString() suite.verifyOK(client.Set(key2, "value")) @@ -2552,29 +2542,21 @@ func (suite *GlideTestSuite) TestLRem() { assert.Equal(suite.T(), int64(2), res2) res3, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value2"), - api.CreateStringResult("value2"), - api.CreateStringResult("value1"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value2", "value2", "value1"}, res3) res4, err := client.LRem(key, -1, "value2") assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), res4) res5, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value2"), api.CreateStringResult("value1")}, res5) + assert.Equal(suite.T(), []string{"value2", "value1"}, res5) res6, err := client.LRem(key, 0, "value2") assert.Nil(suite.T(), err) assert.Equal(suite.T(), int64(1), res6) res7, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value1")}, res7) + assert.Equal(suite.T(), []string{"value1"}, res7) res8, err := client.LRem("non_existing_key", 0, "value") assert.Nil(suite.T(), err) @@ -2598,14 +2580,14 @@ func (suite *GlideTestSuite) TestRPopAndRPopCount() { res3, err := client.RPopCount(key, int64(2)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult("value3"), api.CreateStringResult("value2")}, res3) + assert.Equal(suite.T(), []string{"value3", "value2"}, res3) res4, err := client.RPop("non_existing_key") assert.Nil(suite.T(), err) assert.Equal(suite.T(), api.CreateNilStringResult(), res4) res5, err := client.RPopCount("non_existing_key", int64(2)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res5) + assert.Nil(suite.T(), res5) assert.Nil(suite.T(), err) key2 := uuid.NewString() @@ -2617,7 +2599,7 @@ func (suite *GlideTestSuite) TestRPopAndRPopCount() { assert.IsType(suite.T(), &api.RequestError{}, err) res7, err := client.RPopCount(key2, int64(2)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2642,18 +2624,7 @@ func (suite *GlideTestSuite) TestLInsert() { res4, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value1.5"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value3.5"), - api.CreateStringResult("value4"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"value1", "value1.5", "value2", "value3", "value3.5", "value4"}, res4) res5, err := client.LInsert("non_existing_key", api.Before, "pivot", "elem") assert.Nil(suite.T(), err) @@ -2684,17 +2655,17 @@ func (suite *GlideTestSuite) TestBLPop() { res2, err := client.BLPop([]string{listKey1, listKey2}, float64(0.5)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult(listKey1), api.CreateStringResult("value2")}, res2) + assert.Equal(suite.T(), []string{listKey1, "value2"}, res2) res3, err := client.BLPop([]string{listKey2}, float64(1.0)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res3) + assert.Nil(suite.T(), res3) key := uuid.NewString() suite.verifyOK(client.Set(key, "value")) res4, err := client.BLPop([]string{key}, float64(1.0)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2711,17 +2682,17 @@ func (suite *GlideTestSuite) TestBRPop() { res2, err := client.BRPop([]string{listKey1, listKey2}, float64(0.5)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{api.CreateStringResult(listKey1), api.CreateStringResult("value1")}, res2) + assert.Equal(suite.T(), []string{listKey1, "value1"}, res2) res3, err := client.BRPop([]string{listKey2}, float64(1.0)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res3) + assert.Nil(suite.T(), res3) key := uuid.NewString() suite.verifyOK(client.Set(key, "value")) res4, err := client.BRPop([]string{key}, float64(1.0)) - assert.Equal(suite.T(), ([]api.Result[string])(nil), res4) + assert.Nil(suite.T(), res4) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2743,16 +2714,7 @@ func (suite *GlideTestSuite) TestRPushX() { res3, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value1"), - api.CreateStringResult("value2"), - api.CreateStringResult("value3"), - api.CreateStringResult("value4"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value1", "value2", "value3", "value4"}, res3) res4, err := client.RPushX(key2, []string{"value1"}) assert.Nil(suite.T(), err) @@ -2760,7 +2722,7 @@ func (suite *GlideTestSuite) TestRPushX() { res5, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res5) + assert.Empty(suite.T(), res5) suite.verifyOK(client.Set(key3, "value")) @@ -2792,16 +2754,7 @@ func (suite *GlideTestSuite) TestLPushX() { res3, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("value4"), - api.CreateStringResult("value3"), - api.CreateStringResult("value2"), - api.CreateStringResult("value1"), - }, - res3, - ) + assert.Equal(suite.T(), []string{"value4", "value3", "value2", "value1"}, res3) res4, err := client.LPushX(key2, []string{"value1"}) assert.Nil(suite.T(), err) @@ -2809,7 +2762,7 @@ func (suite *GlideTestSuite) TestLPushX() { res5, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), []api.Result[string]{}, res5) + assert.Empty(suite.T(), res5) suite.verifyOK(client.Set(key3, "value")) @@ -2836,11 +2789,11 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { res1, err := client.LMPop([]string{key1}, api.Left) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res1) + assert.Nil(suite.T(), res1) res2, err := client.LMPopCount([]string{key1}, api.Left, int64(1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) res3, err := client.LPush(key1, []string{"one", "two", "three", "four", "five"}) assert.Nil(suite.T(), err) @@ -2853,7 +2806,7 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{api.CreateStringResult(key1): {api.CreateStringResult("five")}}, + map[string][]string{key1: {"five"}}, res5, ) @@ -2861,8 +2814,8 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{ - api.CreateStringResult(key2): {api.CreateStringResult("one"), api.CreateStringResult("two")}, + map[string][]string{ + key2: {"one", "two"}, }, res6, ) @@ -2870,12 +2823,12 @@ func (suite *GlideTestSuite) TestLMPopAndLMPopCount() { suite.verifyOK(client.Set(key3, "value")) res7, err := client.LMPop([]string{key3}, api.Left) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) res8, err := client.LMPop([]string{key3}, "Invalid") - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res8) + assert.Nil(suite.T(), res8) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2892,11 +2845,11 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { res1, err := client.BLMPop([]string{key1}, api.Left, float64(0.1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res1) + assert.Nil(suite.T(), res1) res2, err := client.BLMPopCount([]string{key1}, api.Left, int64(1), float64(0.1)) assert.Nil(suite.T(), err) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res2) + assert.Nil(suite.T(), res2) res3, err := client.LPush(key1, []string{"one", "two", "three", "four", "five"}) assert.Nil(suite.T(), err) @@ -2909,7 +2862,7 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{api.CreateStringResult(key1): {api.CreateStringResult("five")}}, + map[string][]string{key1: {"five"}}, res5, ) @@ -2917,8 +2870,8 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { assert.Nil(suite.T(), err) assert.Equal( suite.T(), - map[api.Result[string]][]api.Result[string]{ - api.CreateStringResult(key2): {api.CreateStringResult("one"), api.CreateStringResult("two")}, + map[string][]string{ + key2: {"one", "two"}, }, res6, ) @@ -2926,7 +2879,7 @@ func (suite *GlideTestSuite) TestBLMPopAndBLMPopCount() { suite.verifyOK(client.Set(key3, "value")) res7, err := client.BLMPop([]string{key3}, api.Left, float64(0.1)) - assert.Equal(suite.T(), (map[api.Result[string]][]api.Result[string])(nil), res7) + assert.Nil(suite.T(), res7) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -2953,31 +2906,13 @@ func (suite *GlideTestSuite) TestLSet() { res5, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("zero"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - api.CreateStringResult("four"), - }, - res5, - ) + assert.Equal(suite.T(), []string{"zero", "two", "three", "four"}, res5) suite.verifyOK(client.LSet(key, int64(-1), "zero")) res7, err := client.LRange(key, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("zero"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - api.CreateStringResult("zero"), - }, - res7, - ) + assert.Equal(suite.T(), []string{"zero", "two", "three", "zero"}, res7) }) } @@ -3006,15 +2941,7 @@ func (suite *GlideTestSuite) TestLMove() { res4, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res4) // source and destination are the same, performing list rotation, "one" gets popped and added back res5, err := client.LMove(key1, key1, api.Left, api.Left) @@ -3023,15 +2950,7 @@ func (suite *GlideTestSuite) TestLMove() { res6, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res6, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res6) // normal use case, "three" gets popped and added to the left of destination res7, err := client.LPush(key2, []string{"six", "five", "four"}) assert.Nil(suite.T(), err) @@ -3043,26 +2962,10 @@ func (suite *GlideTestSuite) TestLMove() { res9, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - }, - res9, - ) + assert.Equal(suite.T(), []string{"one", "two"}, res9) res10, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("three"), - api.CreateStringResult("four"), - api.CreateStringResult("five"), - api.CreateStringResult("six"), - }, - res10, - ) + assert.Equal(suite.T(), []string{"three", "four", "five", "six"}, res10) // source exists but is not a list type key suite.verifyOK(client.Set(nonListKey, "value")) @@ -3813,6 +3716,200 @@ 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) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"1", "2", "4", "5", "10"}, 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) + }) +} + +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) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"50", "40", "30", "20", "10"}, 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) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + resultList := []string{"apple", "banana", "cherry", "date", "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) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"20", "30", "40"}, 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") @@ -3838,15 +3935,7 @@ func (suite *GlideTestSuite) TestBLMove() { res4, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res4, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res4) // source and destination are the same, performing list rotation, "one" gets popped and added back res5, err := client.BLMove(key1, key1, api.Left, api.Left, float64(0.1)) @@ -3855,15 +3944,7 @@ func (suite *GlideTestSuite) TestBLMove() { res6, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - api.CreateStringResult("three"), - }, - res6, - ) + assert.Equal(suite.T(), []string{"one", "two", "three"}, res6) // normal use case, "three" gets popped and added to the left of destination res7, err := client.LPush(key2, []string{"six", "five", "four"}) assert.Nil(suite.T(), err) @@ -3875,26 +3956,11 @@ func (suite *GlideTestSuite) TestBLMove() { res9, err := client.LRange(key1, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("one"), - api.CreateStringResult("two"), - }, - res9, - ) + assert.Equal(suite.T(), []string{"one", "two"}, res9) + res10, err := client.LRange(key2, int64(0), int64(-1)) assert.Nil(suite.T(), err) - assert.Equal( - suite.T(), - []api.Result[string]{ - api.CreateStringResult("three"), - api.CreateStringResult("four"), - api.CreateStringResult("five"), - api.CreateStringResult("six"), - }, - res10, - ) + assert.Equal(suite.T(), []string{"three", "four", "five", "six"}, res10) // source exists but is not a list type key suite.verifyOK(client.Set(nonListKey, "value")) @@ -3951,7 +4017,7 @@ func (suite *GlideTestSuite) TestType() { suite.verifyOK(client.Set(keyName, initialValue)) result, err := client.Type(keyName) assert.Nil(suite.T(), err) - assert.IsType(suite.T(), result, api.CreateStringResult("string"), "Value is string") + assert.IsType(suite.T(), result, "string", "Value is string") // Test 2: Check if the value is list key1 := "{keylist}-1" + uuid.NewString() @@ -3960,7 +4026,7 @@ func (suite *GlideTestSuite) TestType() { assert.Nil(suite.T(), err) resultType, err := client.Type(key1) assert.Nil(suite.T(), err) - assert.IsType(suite.T(), resultType, api.CreateStringResult("list"), "Value is list") + assert.IsType(suite.T(), resultType, "list", "Value is list") }) } @@ -4012,7 +4078,7 @@ func (suite *GlideTestSuite) TestRename() { // Test 2 Check if the rename command return false if the key/newkey is invalid. key1 := "{keyName}" + uuid.NewString() res1, err := client.Rename(key1, "invalidKey") - assert.Equal(suite.T(), "", res1.Value()) + assert.Equal(suite.T(), "", res1) assert.NotNil(suite.T(), err) assert.IsType(suite.T(), &api.RequestError{}, err) }) @@ -4431,18 +4497,106 @@ func (suite *GlideTestSuite) TestXRead() { }) } -func (suite *GlideTestSuite) TestZAddAndZAddIncr() { +func (suite *GlideTestSuite) TestXGroupSetId() { suite.runWithDefaultClients(func(client api.BaseClient) { - key := uuid.New().String() - key2 := uuid.New().String() - key3 := uuid.New().String() - key4 := uuid.New().String() - membersScoreMap := map[string]float64{ - "one": 1.0, - "two": 2.0, - "three": 3.0, - } - t := suite.T() + key := uuid.NewString() + group := uuid.NewString() + consumer := uuid.NewString() + + // Setup: Create stream with 3 entries, create consumer group, read entries to add them to the Pending Entries List + xadd, err := client.XAddWithOptions( + key, + [][]string{{"f0", "v0"}}, + options.NewXAddOptions().SetId("1-0"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "1-0", xadd.Value()) + xadd, err = client.XAddWithOptions( + key, + [][]string{{"f1", "v1"}}, + options.NewXAddOptions().SetId("1-1"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "1-1", xadd.Value()) + xadd, err = client.XAddWithOptions( + key, + [][]string{{"f2", "v2"}}, + options.NewXAddOptions().SetId("1-2"), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "1-2", xadd.Value()) + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key, group, "0"}, + "Can't send XGROUP CREATE as a custom command", + ) + + xreadgroup, err := client.XReadGroup(group, consumer, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + "1-0": {{"f0", "v0"}}, + "1-1": {{"f1", "v1"}}, + "1-2": {{"f2", "v2"}}, + }, + }, xreadgroup) + + // Sanity check: xreadgroup should not return more entries since they're all already in the + // Pending Entries List. + xreadgroup, err = client.XReadGroup(group, consumer, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Nil(suite.T(), xreadgroup) + + // Reset the last delivered ID for the consumer group to "1-1" + if suite.serverVersion < "7.0.0" { + suite.verifyOK(client.XGroupSetId(key, group, "1-1")) + } else { + opts := options.NewXGroupSetIdOptionsOptions().SetEntriesRead(42) + suite.verifyOK(client.XGroupSetIdWithOptions(key, group, "1-1", opts)) + } + + // xreadgroup should only return entry 1-2 since we reset the last delivered ID to 1-1 + xreadgroup, err = client.XReadGroup(group, consumer, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + "1-2": {{"f2", "v2"}}, + }, + }, xreadgroup) + + // An error is raised if XGROUP SETID is called with a non-existing key + _, err = client.XGroupSetId(uuid.NewString(), group, "1-1") + assert.IsType(suite.T(), &api.RequestError{}, err) + + // An error is raised if XGROUP SETID is called with a non-existing group + _, err = client.XGroupSetId(key, uuid.NewString(), "1-1") + assert.IsType(suite.T(), &api.RequestError{}, err) + + // Setting the ID to a non-existing ID is allowed + suite.verifyOK(client.XGroupSetId(key, group, "99-99")) + + // key exists, but is not a stream + key = uuid.NewString() + suite.verifyOK(client.Set(key, "xgroup setid")) + _, err = client.XGroupSetId(key, group, "1-1") + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestZAddAndZAddIncr() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + key2 := uuid.New().String() + key3 := uuid.New().String() + key4 := uuid.New().String() + membersScoreMap := map[string]float64{ + "one": 1.0, + "two": 2.0, + "three": 3.0, + } + t := suite.T() res, err := client.ZAdd(key, membersScoreMap) assert.Nil(t, err) @@ -4611,14 +4765,11 @@ func (suite *GlideTestSuite) TestZPopMin() { res2, err := client.ZPopMin(key1) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 1) - assert.Equal(suite.T(), float64(1.0), res2[api.CreateStringResult("one")].Value()) + assert.Equal(suite.T(), map[string]float64{"one": float64(1)}, res2) res3, err := client.ZPopMinWithCount(key1, 2) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res3, 2) - assert.Equal(suite.T(), float64(2.0), res3[api.CreateStringResult("two")].Value()) - assert.Equal(suite.T(), float64(3.0), res3[api.CreateStringResult("three")].Value()) + assert.Equal(suite.T(), map[string]float64{"two": float64(2), "three": float64(3)}, res3) // non sorted set key _, err = client.Set(key2, "test") @@ -4645,14 +4796,11 @@ func (suite *GlideTestSuite) TestZPopMax() { res2, err := client.ZPopMax(key1) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res2, 1) - assert.Equal(suite.T(), float64(3.0), res2[api.CreateStringResult("three")].Value()) + assert.Equal(suite.T(), map[string]float64{"three": float64(3)}, res2) res3, err := client.ZPopMaxWithCount(key1, 2) assert.Nil(suite.T(), err) - assert.Len(suite.T(), res3, 2) - assert.Equal(suite.T(), float64(2.0), res3[api.CreateStringResult("two")].Value()) - assert.Equal(suite.T(), float64(1.0), res3[api.CreateStringResult("one")].Value()) + assert.Equal(suite.T(), map[string]float64{"two": float64(2), "one": float64(1)}, res3) // non sorted set key _, err = client.Set(key2, "test") @@ -4713,21 +4861,12 @@ func (suite *GlideTestSuite) TestZRange() { assert.NoError(t, err) // index [0:1] res, err := client.ZRange(key, options.NewRangeByIndexQuery(0, 1)) - expected := []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // index [0:-1] (all) res, err = client.ZRange(key, options.NewRangeByIndexQuery(0, -1)) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b", "c"}, res) // index [3:1] (none) res, err = client.ZRange(key, options.NewRangeByIndexQuery(3, 1)) assert.NoError(t, err) @@ -4738,48 +4877,31 @@ func (suite *GlideTestSuite) TestZRange() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, true)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b", "c"}, res) // score [-inf:3) query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, false)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // score (3:-inf] reverse query = options.NewRangeByScoreQuery( options.NewScoreBoundary(3, false), options.NewInfiniteScoreBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // score [-inf:+inf] limit 1 2 query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewInfiniteScoreBoundary(options.PositiveInfinity)). SetLimit(1, 2) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("c"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "c"}, res) // score [-inf:3) reverse (none) query = options.NewRangeByScoreQuery( options.NewInfiniteScoreBoundary(options.NegativeInfinity), @@ -4800,36 +4922,24 @@ func (suite *GlideTestSuite) TestZRange() { options.NewInfiniteLexBoundary(options.NegativeInfinity), options.NewLexBoundary("c", false)) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("a"), - api.CreateStringResult("b"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"a", "b"}, res) // lex [+:-] reverse limit 1 2 query = options.NewRangeByLexQuery( options.NewInfiniteLexBoundary(options.PositiveInfinity), options.NewInfiniteLexBoundary(options.NegativeInfinity)). SetReverse().SetLimit(1, 2) res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // lex (c:-] reverse query = options.NewRangeByLexQuery( options.NewLexBoundary("c", false), options.NewInfiniteLexBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRange(key, query) - expected = []api.Result[string]{ - api.CreateStringResult("b"), - api.CreateStringResult("a"), - } assert.NoError(t, err) - assert.Equal(t, expected, res) + assert.Equal(t, []string{"b", "a"}, res) // lex [+:c] (none) query = options.NewRangeByLexQuery( options.NewInfiniteLexBoundary(options.PositiveInfinity), @@ -4853,18 +4963,18 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { assert.NoError(t, err) // index [0:1] res, err := client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, 1)) - expected := map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + expected := map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), } assert.NoError(t, err) assert.Equal(t, expected, res) // index [0:-1] (all) res, err = client.ZRangeWithScores(key, options.NewRangeByIndexQuery(0, -1)) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4877,10 +4987,10 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, true)) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4889,9 +4999,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity), options.NewScoreBoundary(3, false)) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), + expected = map[string]float64{ + "a": float64(1.0), + "b": float64(2.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4901,9 +5011,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.NegativeInfinity)). SetReverse() res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("a"): api.CreateFloat64Result(1.0), + expected = map[string]float64{ + "b": float64(2.0), + "a": float64(1.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -4913,9 +5023,9 @@ func (suite *GlideTestSuite) TestZRangeWithScores() { options.NewInfiniteScoreBoundary(options.PositiveInfinity)). SetLimit(1, 2) res, err = client.ZRangeWithScores(key, query) - expected = map[api.Result[string]]api.Result[float64]{ - api.CreateStringResult("b"): api.CreateFloat64Result(2.0), - api.CreateStringResult("c"): api.CreateFloat64Result(3.0), + expected = map[string]float64{ + "b": float64(2.0), + "c": float64(3.0), } assert.NoError(t, err) assert.Equal(t, expected, res) @@ -5646,15 +5756,18 @@ func (suite *GlideTestSuite) TestXPendingFailures() { consumer1 := "consumer-1-" + uuid.New().String() invalidConsumer := "invalid-consumer-" + uuid.New().String() - command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + suite.verifyOK( + client.XGroupCreateWithOptions( + key, + groupName, + zeroStreamId, + options.NewXGroupCreateOptions().SetMakeStream(), + ), + ) + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.(string)) - - command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} - resp, err = client.CustomCommand(command) - assert.NoError(suite.T(), err) assert.True(suite.T(), resp.(bool)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) @@ -5798,15 +5911,13 @@ func (suite *GlideTestSuite) TestXPendingFailures() { consumer1 := "consumer-1-" + uuid.New().String() invalidConsumer := "invalid-consumer-" + uuid.New().String() - command := []string{"XGroup", "Create", key, groupName, zeroStreamId, "MKSTREAM"} + suite.verifyOK( + client.XGroupCreateWithOptions(key, groupName, zeroStreamId, options.NewXGroupCreateOptions().SetMakeStream()), + ) + command := []string{"XGroup", "CreateConsumer", key, groupName, consumer1} resp, err := client.CustomCommand(command) assert.NoError(suite.T(), err) - assert.Equal(suite.T(), "OK", resp.Value().(string)) - - command = []string{"XGroup", "CreateConsumer", key, groupName, consumer1} - resp, err = client.CustomCommand(command) - assert.NoError(suite.T(), err) assert.True(suite.T(), resp.Value().(bool)) _, err = client.XAdd(key, [][]string{{"field1", "value1"}}) @@ -5954,6 +6065,55 @@ func (suite *GlideTestSuite) TestXPendingFailures() { }) } +func (suite *GlideTestSuite) TestXGroupCreate_XGroupDestroy() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.NewString() + group := uuid.NewString() + id := "0-1" + + // Stream not created results in error + _, err := client.XGroupCreate(key, group, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // Stream with option to create creates stream & Group + opts := options.NewXGroupCreateOptions().SetMakeStream() + suite.verifyOK(client.XGroupCreateWithOptions(key, group, id, opts)) + + // ...and again results in BUSYGROUP error, because group names must be unique + _, err = client.XGroupCreate(key, group, id) + assert.ErrorContains(suite.T(), err, "BUSYGROUP") + assert.IsType(suite.T(), &api.RequestError{}, err) + + // Stream Group can be destroyed returns: true + destroyed, err := client.XGroupDestroy(key, group) + assert.NoError(suite.T(), err) + assert.True(suite.T(), destroyed) + + // ...and again results in: false + destroyed, err = client.XGroupDestroy(key, group) + assert.NoError(suite.T(), err) + assert.False(suite.T(), destroyed) + + // ENTRIESREAD option was added in valkey 7.0.0 + opts = options.NewXGroupCreateOptions().SetEntriesRead(100) + if suite.serverVersion >= "7.0.0" { + suite.verifyOK(client.XGroupCreateWithOptions(key, group, id, opts)) + } else { + _, err = client.XGroupCreateWithOptions(key, group, id, opts) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + } + + // key is not a stream + key = uuid.NewString() + suite.verifyOK(client.Set(key, id)) + _, err = client.XGroupCreate(key, group, id) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + func (suite *GlideTestSuite) TestObjectEncoding() { suite.runWithDefaultClients(func(client api.BaseClient) { // Test 1: Check object encoding for embstr @@ -6231,3 +6391,822 @@ 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)) + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, keyValueMap, 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)) + resultConfig, err := defaultClient.ConfigGet([]string{"maxmemory-policy"}) + assert.Nil(t, err, "Failed to get configuration") + assert.Equal(t, keyValueMap, 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) + + sortedValues, err := client.LRange(sortedKey, 0, -1) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []string{"d", "b", "c", "e", "a"}, sortedValues) + }) +} + +func (suite *GlideTestSuite) TestXGroupStreamCommands() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + stringKey := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumerName := "consumer-" + uuid.New().String() + + sendWithCustomCommand( + suite, + client, + []string{"xgroup", "create", key, groupName, zeroStreamId, "MKSTREAM"}, + "Can't send XGROUP CREATE as a custom command", + ) + respBool, err := client.XGroupCreateConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.True(suite.T(), respBool) + + // create a consumer for a group that doesn't exist should result in a NOGROUP error + _, err = client.XGroupCreateConsumer(key, "non-existent-group", consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.True(suite.T(), strings.Contains(err.Error(), "NOGROUP")) + + // create consumer that already exists should return false + respBool, err = client.XGroupCreateConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.False(suite.T(), respBool) + + // Delete a consumer that hasn't been created should return 0 + respInt64, err := client.XGroupDelConsumer(key, groupName, "non-existent-consumer") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), respInt64) + + // Add two stream entries + streamId1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamId2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // read the stream for the consumer and mark messages as pending + expectedGroup := map[string]map[string][][]string{ + key: {streamId1.Value(): {{"field1", "value1"}}, streamId2.Value(): {{"field2", "value2"}}}, + } + actualGroup, err := client.XReadGroup(groupName, consumerName, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.True(suite.T(), reflect.DeepEqual(expectedGroup, actualGroup), + "Expected and actual results do not match", + ) + + // delete one of the streams using XDel + respInt64, err = client.XDel(key, []string{streamId1.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), respInt64) + + // xreadgroup should return one empty stream and one non-empty stream + resp, err := client.XReadGroup(groupName, consumerName, map[string]string{key: zeroStreamId}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string]map[string][][]string{ + key: { + streamId1.Value(): nil, + streamId2.Value(): {{"field2", "value2"}}, + }, + }, resp) + + // add a new stream entry + streamId3, err := client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), streamId3) + + // xack that streamid1 and streamid2 have been processed + xackResult, err := client.XAck(key, groupName, []string{streamId1.Value(), streamId2.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(2), xackResult) + + // Delete the consumer group and expect 0 pending messages + respInt64, err = client.XGroupDelConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), respInt64) + + // xack streamid_1, and streamid_2 already received returns 0L + xackResult, err = client.XAck(key, groupName, []string{streamId1.Value(), streamId2.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), xackResult) + + // Consume the last message with the previously deleted consumer (creates the consumer anew) + resp, err = client.XReadGroup(groupName, consumerName, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(resp[key])) + + // Use non existent group, so xack streamid_3 returns 0 + xackResult, err = client.XAck(key, "non-existent-group", []string{streamId3.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), xackResult) + + // Delete the consumer group and expect 1 pending message + respInt64, err = client.XGroupDelConsumer(key, groupName, consumerName) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), respInt64) + + // Set a string key, and expect an error when you try to create or delete a consumer group + _, err = client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + _, err = client.XGroupCreateConsumer(stringKey, groupName, consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XGroupDelConsumer(stringKey, groupName, consumerName) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} + +func (suite *GlideTestSuite) TestSetBit_SetSingleBit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + resultInt64, err := client.SetBit(key, 7, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + result, err := client.Get(key) + assert.NoError(suite.T(), err) + assert.Contains(suite.T(), result.Value(), "\x01") + }) +} + +func (suite *GlideTestSuite) TestSetBit_SetAndCheckPreviousBit() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + resultInt64, err := client.SetBit(key, 7, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + resultInt64, err = client.SetBit(key, 7, 0) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(1), resultInt64) + }) +} + +func (suite *GlideTestSuite) TestSetBit_SetMultipleBits() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + var resultInt64 int64 + + resultInt64, err := client.SetBit(key, 3, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + resultInt64, err = client.SetBit(key, 5, 1) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), resultInt64) + + result, err := client.Get(key) + assert.NoError(suite.T(), err) + value := result.Value() + + binaryString := fmt.Sprintf("%08b", value[0]) + + assert.Equal(suite.T(), "00010100", binaryString) + }) +} + +func (suite *GlideTestSuite) TestWait() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + client.Set(key, "test") + // Test 1: numberOfReplicas (2) + resultInt64, err := client.Wait(2, 2000) + assert.NoError(suite.T(), err) + assert.True(suite.T(), resultInt64 >= 2) + + // Test 2: Invalid timeout (negative) + _, err = client.Wait(2, -1) + + // Assert error and message for invalid timeout + assert.NotNil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestGetBit_ExistingKey_ValidOffset() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + offset := int64(7) + value := int64(1) + + client.SetBit(key, offset, value) + + result, err := client.GetBit(key, offset) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), value, result) + }) +} + +func (suite *GlideTestSuite) TestGetBit_NonExistentKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + offset := int64(10) + + result, err := client.GetBit(key, offset) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), result) + }) +} + +func (suite *GlideTestSuite) TestGetBit_InvalidOffset() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + invalidOffset := int64(-1) + + _, err := client.GetBit(key, invalidOffset) + assert.NotNil(suite.T(), err) + }) +} + +func (suite *GlideTestSuite) TestBitCount_ExistingKey() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + for i := int64(0); i < 8; i++ { + client.SetBit(key, i, 1) + } + + result, err := client.BitCount(key) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(8), result) + }) +} + +func (suite *GlideTestSuite) TestBitCount_ZeroBits() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + + result, err := client.BitCount(key) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(0), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEnd() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(19), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEndByte() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + opts.SetBitmapIndexType(options.BYTE) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(19), result) + }) +} + +func (suite *GlideTestSuite) TestBitCountWithOptions_StartEndBit() { + suite.SkipIfServerVersionLowerThanBy("7.0.0") + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + value := "TestBitCountWithOptions_StartEnd" + + client.Set(key, value) + + start := int64(1) + end := int64(5) + opts := &options.BitCountOptions{} + opts.SetStart(start) + opts.SetEnd(end) + opts.SetBitmapIndexType(options.BIT) + + result, err := client.BitCountWithOptions(key, opts) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(3), result) + }) +} + +func (suite *GlideTestSuite) TestXPendingAndXClaim() { + suite.runWithDefaultClients(func(client api.BaseClient) { + // 1. Arrange the data + key := uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + consumer2 := "consumer-2-" + uuid.New().String() + + resp, err := client.XGroupCreateWithOptions( + key, + groupName, + zeroStreamId, + options.NewXGroupCreateOptions().SetMakeStream(), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", resp) + + respBool, err := client.XGroupCreateConsumer(key, groupName, consumer1) + assert.NoError(suite.T(), err) + assert.True(suite.T(), respBool) + + respBool, err = client.XGroupCreateConsumer(key, groupName, consumer2) + assert.NoError(suite.T(), err) + assert.True(suite.T(), respBool) + + // Add two stream entries for consumer 1 + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + streamid_2, err := client.XAdd(key, [][]string{{"field2", "value2"}}) + assert.NoError(suite.T(), err) + + // Read the stream entries for consumer 1 and mark messages as pending + xReadGroupResult1, err := client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + expectedResult := map[string]map[string][][]string{ + key: { + streamid_1.Value(): {{"field1", "value1"}}, + streamid_2.Value(): {{"field2", "value2"}}, + }, + } + assert.Equal(suite.T(), expectedResult, xReadGroupResult1) + + // Add 3 more stream entries for consumer 2 + streamid_3, err := client.XAdd(key, [][]string{{"field3", "value3"}}) + assert.NoError(suite.T(), err) + streamid_4, err := client.XAdd(key, [][]string{{"field4", "value4"}}) + assert.NoError(suite.T(), err) + streamid_5, err := client.XAdd(key, [][]string{{"field5", "value5"}}) + assert.NoError(suite.T(), err) + + // read the entire stream for consumer 2 and mark messages as pending + xReadGroupResult2, err := client.XReadGroup(groupName, consumer2, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + expectedResult2 := map[string]map[string][][]string{ + key: { + streamid_3.Value(): {{"field3", "value3"}}, + streamid_4.Value(): {{"field4", "value4"}}, + streamid_5.Value(): {{"field5", "value5"}}, + }, + } + assert.Equal(suite.T(), expectedResult2, xReadGroupResult2) + + expectedSummary := api.XPendingSummary{ + NumOfMessages: 5, + StartId: streamid_1, + EndId: streamid_5, + ConsumerMessages: []api.ConsumerPendingMessage{ + {ConsumerName: consumer1, MessageCount: 2}, + {ConsumerName: consumer2, MessageCount: 3}, + }, + } + summaryResult, err := client.XPending(key, groupName) + assert.NoError(suite.T(), err) + assert.True( + suite.T(), + reflect.DeepEqual(expectedSummary, summaryResult), + "Expected and actual results do not match", + ) + + // ensure idle time > 0 + time.Sleep(2000 * time.Millisecond) + pendingResultExtended, err := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10), + ) + assert.NoError(suite.T(), err) + + assert.Greater(suite.T(), len(pendingResultExtended), 2) + // because of the idle time return, we have to exclude it from the expected result + // and check separately + assert.Equal(suite.T(), pendingResultExtended[0].Id, streamid_1.Value()) + assert.Equal(suite.T(), pendingResultExtended[0].ConsumerName, consumer1) + assert.GreaterOrEqual(suite.T(), pendingResultExtended[0].DeliveryCount, int64(0)) + + assert.Equal(suite.T(), pendingResultExtended[1].Id, streamid_2.Value()) + assert.Equal(suite.T(), pendingResultExtended[1].ConsumerName, consumer1) + assert.GreaterOrEqual(suite.T(), pendingResultExtended[1].DeliveryCount, int64(0)) + + assert.Equal(suite.T(), pendingResultExtended[2].Id, streamid_3.Value()) + assert.Equal(suite.T(), pendingResultExtended[2].ConsumerName, consumer2) + assert.GreaterOrEqual(suite.T(), pendingResultExtended[2].DeliveryCount, int64(0)) + + assert.Equal(suite.T(), pendingResultExtended[3].Id, streamid_4.Value()) + assert.Equal(suite.T(), pendingResultExtended[3].ConsumerName, consumer2) + assert.GreaterOrEqual(suite.T(), pendingResultExtended[3].DeliveryCount, int64(0)) + + assert.Equal(suite.T(), pendingResultExtended[4].Id, streamid_5.Value()) + assert.Equal(suite.T(), pendingResultExtended[4].ConsumerName, consumer2) + assert.GreaterOrEqual(suite.T(), pendingResultExtended[4].DeliveryCount, int64(0)) + + // use claim to claim stream 3 and 5 for consumer 1 + claimResult, err := client.XClaim( + key, + groupName, + consumer1, + int64(0), + []string{streamid_3.Value(), streamid_5.Value()}, + ) + assert.NoError(suite.T(), err) + expectedClaimResult := map[string][][]string{ + streamid_3.Value(): {{"field3", "value3"}}, + streamid_5.Value(): {{"field5", "value5"}}, + } + assert.Equal(suite.T(), expectedClaimResult, claimResult) + + claimResultJustId, err := client.XClaimJustId( + key, + groupName, + consumer1, + int64(0), + []string{streamid_3.Value(), streamid_5.Value()}, + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), []string{streamid_3.Value(), streamid_5.Value()}, claimResultJustId) + + // add one more stream + streamid_6, err := client.XAdd(key, [][]string{{"field6", "value6"}}) + assert.NoError(suite.T(), err) + + // using force, we can xclaim the message without reading it + claimResult, err = client.XClaimWithOptions( + key, + groupName, + consumer1, + int64(0), + []string{streamid_6.Value()}, + options.NewStreamClaimOptions().SetForce().SetRetryCount(99), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), map[string][][]string{streamid_6.Value(): {{"field6", "value6"}}}, claimResult) + + forcePendingResult, err := client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions(streamid_6.Value(), streamid_6.Value(), 1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(forcePendingResult)) + assert.Equal(suite.T(), streamid_6.Value(), forcePendingResult[0].Id) + assert.Equal(suite.T(), consumer1, forcePendingResult[0].ConsumerName) + assert.Equal(suite.T(), int64(99), forcePendingResult[0].DeliveryCount) + + // acknowledge streams 2, 3, 4 and 6 and remove them from xpending results + xackResult, err := client.XAck( + key, groupName, + []string{streamid_2.Value(), streamid_3.Value(), streamid_4.Value(), streamid_6.Value()}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), int64(4), xackResult) + + pendingResultExtended, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions(streamid_3.Value(), "+", 10), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(pendingResultExtended)) + assert.Equal(suite.T(), streamid_5.Value(), pendingResultExtended[0].Id) + assert.Equal(suite.T(), consumer1, pendingResultExtended[0].ConsumerName) + + pendingResultExtended, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "("+streamid_5.Value(), 10), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(pendingResultExtended)) + assert.Equal(suite.T(), streamid_1.Value(), pendingResultExtended[0].Id) + assert.Equal(suite.T(), consumer1, pendingResultExtended[0].ConsumerName) + + pendingResultExtended, err = client.XPendingWithOptions( + key, + groupName, + options.NewXPendingOptions("-", "+", 10).SetMinIdleTime(1).SetConsumer(consumer1), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 2, len(pendingResultExtended)) + }) +} + +func (suite *GlideTestSuite) TestXClaimFailure() { + suite.runWithDefaultClients(func(client api.BaseClient) { + key := uuid.New().String() + stringKey := "string-key-" + uuid.New().String() + groupName := "group" + uuid.New().String() + zeroStreamId := "0" + consumer1 := "consumer-1-" + uuid.New().String() + + // create group and consumer for the group + groupCreateResult, err := client.XGroupCreateWithOptions( + key, + groupName, + zeroStreamId, + options.NewXGroupCreateOptions().SetMakeStream(), + ) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "OK", groupCreateResult) + + consumerCreateResult, err := client.XGroupCreateConsumer(key, groupName, consumer1) + assert.NoError(suite.T(), err) + assert.True(suite.T(), consumerCreateResult) + + // Add stream entry and mark as pending + streamid_1, err := client.XAdd(key, [][]string{{"field1", "value1"}}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), streamid_1) + + readGroupResult, err := client.XReadGroup(groupName, consumer1, map[string]string{key: ">"}) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), readGroupResult) + + // claim with invalid stream entry IDs + _, err = client.XClaimJustId(key, groupName, consumer1, int64(1), []string{"invalid-stream-id"}) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + // claim with empty stream entry IDs returns empty map + claimResult, err := client.XClaimJustId(key, groupName, consumer1, int64(1), []string{}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), []string{}, claimResult) + + // non existent key causes a RequestError + claimOptions := options.NewStreamClaimOptions().SetIdleTime(1) + _, err = client.XClaim(stringKey, groupName, consumer1, int64(1), []string{streamid_1.Value()}) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.Contains(suite.T(), err.Error(), "NOGROUP") + + _, err = client.XClaimWithOptions( + stringKey, + groupName, + consumer1, + int64(1), + []string{streamid_1.Value()}, + claimOptions, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.Contains(suite.T(), err.Error(), "NOGROUP") + + _, err = client.XClaimJustId(stringKey, groupName, consumer1, int64(1), []string{streamid_1.Value()}) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.Contains(suite.T(), err.Error(), "NOGROUP") + + _, err = client.XClaimJustIdWithOptions( + stringKey, + groupName, + consumer1, + int64(1), + []string{streamid_1.Value()}, + claimOptions, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + assert.Contains(suite.T(), err.Error(), "NOGROUP") + + // key exists, but is not a stream + _, err = client.Set(stringKey, "test") + assert.NoError(suite.T(), err) + _, err = client.XClaim(stringKey, groupName, consumer1, int64(1), []string{streamid_1.Value()}) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XClaimWithOptions( + stringKey, + groupName, + consumer1, + int64(1), + []string{streamid_1.Value()}, + claimOptions, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XClaimJustId(stringKey, groupName, consumer1, int64(1), []string{streamid_1.Value()}) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + + _, err = client.XClaimJustIdWithOptions( + stringKey, + groupName, + consumer1, + int64(1), + []string{streamid_1.Value()}, + claimOptions, + ) + assert.Error(suite.T(), err) + assert.IsType(suite.T(), &api.RequestError{}, err) + }) +} diff --git a/go/integTest/standalone_commands_test.go b/go/integTest/standalone_commands_test.go index 063e884a5d..2d4a0ec31c 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" ) @@ -179,11 +180,7 @@ func (suite *GlideTestSuite) TestConfigSetAndGet_multipleArgs() { suite.T().Skip("This feature is added in version 7") } configMap := map[string]string{"timeout": "1000", "maxmemory": "1GB"} - key1 := api.CreateStringResult("timeout") - value1 := api.CreateStringResult("1000") - key2 := api.CreateStringResult("maxmemory") - value2 := api.CreateStringResult("1073741824") - resultConfigMap := map[api.Result[string]]api.Result[string]{key1: value1, key2: value2} + resultConfigMap := map[string]string{"timeout": "1000", "maxmemory": "1073741824"} suite.verifyOK(client.ConfigSet(configMap)) result2, err := client.ConfigGet([]string{"timeout", "maxmemory"}) @@ -216,7 +213,7 @@ func (suite *GlideTestSuite) TestConfigSetAndGet_invalidArgs() { assert.IsType(suite.T(), &api.RequestError{}, err) result2, err := client.ConfigGet([]string{"time"}) - assert.Equal(suite.T(), map[api.Result[string]]api.Result[string]{}, result2) + assert.Equal(suite.T(), map[string]string{}, result2) assert.Nil(suite.T(), err) } @@ -273,3 +270,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/python/python/glide/__init__.py b/python/python/glide/__init__.py index 4a7ca8328e..9e3d808a8c 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -30,6 +30,7 @@ FunctionRestorePolicy, InfoSection, InsertPosition, + OnlyIfEqual, UpdateOptions, ) from glide.async_commands.server_modules import ft, glide_json, json_transaction @@ -225,6 +226,7 @@ "Script", "ScoreBoundary", "ConditionalChange", + "OnlyIfEqual", "ExpireOptions", "ExpiryGetEx", "ExpirySet", diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 94b5ec4093..efa8310200 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -79,6 +79,19 @@ class ConditionalChange(Enum): ONLY_IF_DOES_NOT_EXIST = "NX" +@dataclass +class OnlyIfEqual: + """ + Change condition to the `SET` command, + For additional conditonal options see ConditionalChange + - comparison_value - value to compare to the current value of a key. + If comparison_value is equal to the key, it will overwrite the value of key to the new provided value + Equivalent to the IFEQ comparison-value in the Valkey API + """ + + comparison_value: TEncodable + + class ExpiryType(Enum): """SET option: The type of the expiry. - SEC - Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. @@ -435,7 +448,7 @@ async def set( self, key: TEncodable, value: TEncodable, - conditional_set: Optional[ConditionalChange] = None, + conditional_set: Optional[Union[ConditionalChange, OnlyIfEqual]] = None, expiry: Optional[ExpirySet] = None, return_old_value: bool = False, ) -> Optional[bytes]: @@ -447,7 +460,7 @@ async def set( key (TEncodable): the key to store. value (TEncodable): the value to store with the given key. conditional_set (Optional[ConditionalChange], optional): set the key only if the given condition is met. - Equivalent to [`XX` | `NX`] in the Valkey API. Defaults to None. + Equivalent to [`XX` | `NX` | `IFEQ` comparison-value] in the Valkey API. Defaults to None. expiry (Optional[ExpirySet], optional): set expiriation to the given key. Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Valkey API. Defaults to None. return_old_value (bool, optional): Return the old value stored at key, or None if key did not exist. @@ -463,16 +476,38 @@ async def set( Example: >>> await client.set(b"key", b"value") 'OK' - >>> await client.set("key", "new_value",conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=Expiry(ExpiryType.SEC, 5)) + + # ONLY_IF_EXISTS -> Only set the key if it already exists + # expiry -> Set the amount of time until key expires + >>> await client.set("key", "new_value",conditional_set=ConditionalChange.ONLY_IF_EXISTS, expiry=ExpirySet(ExpiryType.SEC, 5)) 'OK' # Set "new_value" to "key" only if "key" already exists, and set the key expiration to 5 seconds. + + # ONLY_IF_DOES_NOT_EXIST -> Only set key if it does not already exist >>> await client.set("key", "value", conditional_set=ConditionalChange.ONLY_IF_DOES_NOT_EXIST,return_old_value=True) b'new_value' # Returns the old value of "key". >>> await client.get("key") b'new_value' # Value wasn't modified back to being "value" because of "NX" flag. + + + # ONLY_IF_EQUAL -> Only set key if provided value is equal to current value of the key + >>> await client.set("key", "value") + 'OK' # Reset "key" to "value" + >>> await client.set("key", "new_value", conditional_set=OnlyIfEqual("different_value") + 'None' # Did not rewrite value of "key" because provided value was not equal to the previous value of "key" + >>> await client.get("key") + b'value' # Still the original value because nothing got rewritten in the last call + >>> await client.set("key", "new_value", conditional_set=OnlyIfEqual("value") + 'OK' + >>> await client.get("key") + b'newest_value" # Set "key" to "new_value" because the provided value was equal to the previous value of "key" """ args = [key, value] - if conditional_set: + if isinstance(conditional_set, ConditionalChange): args.append(conditional_set.value) + + elif isinstance(conditional_set, OnlyIfEqual): + args.extend(["IFEQ", conditional_set.comparison_value]) + if return_old_value: args.append("GET") if expiry is not None: diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index bbd1060a40..db17c5ea5d 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -39,6 +39,7 @@ InfBound, InfoSection, InsertPosition, + OnlyIfEqual, UpdateOptions, ) from glide.async_commands.sorted_set import ( @@ -452,6 +453,7 @@ async def test_inflight_request_limit( async def test_conditional_set(self, glide_client: TGlideClient): key = get_random_string(10) value = get_random_string(10) + res = await glide_client.set( key, value, conditional_set=ConditionalChange.ONLY_IF_EXISTS ) @@ -466,6 +468,29 @@ async def test_conditional_set(self, glide_client: TGlideClient): ) assert res is None assert await glide_client.get(key) == value.encode() + # Tests for ONLY_IF_EQUAL below in test_set_only_if_equal() + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.skip_if_version_below("8.1.0") + async def test_set_only_if_equal(self, glide_client: TGlideClient): + key = get_random_string(10) + value = get_random_string(10) + value2 = get_random_string(10) + wrong_comparison_value = get_random_string(10) + while wrong_comparison_value == value: + wrong_comparison_value = get_random_string(10) + + await glide_client.set(key, value) + + res = await glide_client.set( + key, "foobar", conditional_set=OnlyIfEqual(wrong_comparison_value) + ) + assert res is None + assert await glide_client.get(key) == value.encode() + res = await glide_client.set(key, value2, conditional_set=OnlyIfEqual(value)) + assert res == OK + assert await glide_client.get(key) == value2.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])