Skip to content

Commit

Permalink
Python: add HRANDFIELD command (valkey-io#1334)
Browse files Browse the repository at this point in the history
* Python: add HRANDFIELD command

* Use variable for 'WITHVALUES' arg

* PR suggestions

* Remove WITH_VALUES constant

* PR suggestions
  • Loading branch information
aaron-congo authored and cyip10 committed Jun 24, 2024
1 parent 6ab6191 commit eb8a680
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Python: Added GEOPOS command ([#1301](https://github.com/aws/glide-for-redis/pull/1301))
* Node: Added PFADD command ([#1317](https://github.com/aws/glide-for-redis/pull/1317))
* Python: Added PFADD command ([#1315](https://github.com/aws/glide-for-redis/pull/1315))
* Python: Added HRANDFIELD command ([#1334](https://github.com/aws/glide-for-redis/pull/1334))

#### Fixes
* Python: Fix typing error "‘type’ object is not subscriptable" ([#1203](https://github.com/aws/glide-for-redis/pull/1203))
Expand Down
105 changes: 105 additions & 0 deletions glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(crate) enum ExpectedReturnType {
ArrayOfBools,
Lolwut,
ArrayOfArraysOfDoubleOrNull,
ArrayOfKeyValuePairs,
}

pub(crate) fn convert_to_expected_type(
Expand Down Expand Up @@ -268,6 +269,23 @@ pub(crate) fn convert_to_expected_type(
.into()),
}
}
ExpectedReturnType::ArrayOfKeyValuePairs => match value {
Value::Nil => Ok(value),
Value::Array(ref array) if array.is_empty() || matches!(array[0], Value::Array(_)) => {
Ok(value)
}
Value::Array(array)
if matches!(array[0], Value::BulkString(_) | Value::SimpleString(_)) =>
{
convert_flat_array_to_key_value_pairs(array)
}
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to an array of key-value pairs",
format!("(response was {:?})", value),
)
.into()),
},
}
}

Expand Down Expand Up @@ -333,6 +351,26 @@ fn convert_array_to_map(
Ok(Value::Map(map))
}

/// Converts a flat array of values to a two-dimensional array, where the inner arrays are two-length arrays representing key-value pairs. Normally a map would be more suitable for these responses, but some commands (eg HRANDFIELD) may return duplicate key-value pairs depending on the command arguments. These duplicated pairs cannot be represented by a map.
///
/// `array` is a flat array containing keys at even-positioned elements and their associated values at odd-positioned elements.
fn convert_flat_array_to_key_value_pairs(array: Vec<Value>) -> RedisResult<Value> {
if array.len() % 2 != 0 {
return Err((
ErrorKind::TypeError,
"Response has odd number of items, and cannot be converted to an array of key-value pairs"
)
.into());
}

let mut result = Vec::with_capacity(array.len() / 2);
for i in (0..array.len()).step_by(2) {
let pair = vec![array[i].clone(), array[i + 1].clone()];
result.push(Value::Array(pair));
}
Ok(Value::Array(result))
}

pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
let command = cmd.command()?;

Expand All @@ -350,6 +388,9 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble),
b"JSON.TOGGLE" => Some(ExpectedReturnType::JsonToggleReturnType),
b"GEOPOS" => Some(ExpectedReturnType::ArrayOfArraysOfDoubleOrNull),
b"HRANDFIELD" => cmd
.position(b"WITHVALUES")
.map(|_| ExpectedReturnType::ArrayOfKeyValuePairs),
b"ZADD" => cmd
.position(b"INCR")
.map(|_| ExpectedReturnType::DoubleOrNull),
Expand Down Expand Up @@ -455,6 +496,70 @@ mod tests {
assert_eq!(expected_response, converted_response);
}

#[test]
fn convert_hrandfield() {
assert!(matches!(
expected_type_for_cmd(
redis::cmd("HRANDFIELD")
.arg("key")
.arg("1")
.arg("withvalues")
),
Some(ExpectedReturnType::ArrayOfKeyValuePairs)
));

assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key").arg("1")).is_none());
assert!(expected_type_for_cmd(redis::cmd("HRANDFIELD").arg("key")).is_none());

let flat_array = Value::Array(vec![
Value::BulkString(b"key1".to_vec()),
Value::BulkString(b"value1".to_vec()),
Value::BulkString(b"key2".to_vec()),
Value::BulkString(b"value2".to_vec()),
]);
let two_dimensional_array = Value::Array(vec![
Value::Array(vec![
Value::BulkString(b"key1".to_vec()),
Value::BulkString(b"value1".to_vec()),
]),
Value::Array(vec![
Value::BulkString(b"key2".to_vec()),
Value::BulkString(b"value2".to_vec()),
]),
]);
let converted_flat_array =
convert_to_expected_type(flat_array, Some(ExpectedReturnType::ArrayOfKeyValuePairs))
.unwrap();
assert_eq!(two_dimensional_array, converted_flat_array);

let converted_two_dimensional_array = convert_to_expected_type(
two_dimensional_array.clone(),
Some(ExpectedReturnType::ArrayOfKeyValuePairs),
)
.unwrap();
assert_eq!(two_dimensional_array, converted_two_dimensional_array);

let empty_array = Value::Array(vec![]);
let converted_empty_array = convert_to_expected_type(
empty_array.clone(),
Some(ExpectedReturnType::ArrayOfKeyValuePairs),
)
.unwrap();
assert_eq!(empty_array, converted_empty_array);

let converted_nil_value =
convert_to_expected_type(Value::Nil, Some(ExpectedReturnType::ArrayOfKeyValuePairs))
.unwrap();
assert_eq!(Value::Nil, converted_nil_value);

let array_of_doubles = Value::Array(vec![Value::Double(5.5)]);
assert!(convert_to_expected_type(
array_of_doubles,
Some(ExpectedReturnType::ArrayOfKeyValuePairs)
)
.is_err());
}

#[test]
fn convert_zadd_only_if_incr_is_included() {
assert!(matches!(
Expand Down
1 change: 1 addition & 0 deletions glide-core/src/protobuf/redis_request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ enum RequestType {
Touch = 132;
ZRevRank = 133;
ZInterStore = 134;
HRandField = 135;
}

message Command {
Expand Down
3 changes: 3 additions & 0 deletions glide-core/src/request_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub enum RequestType {
Touch = 132,
ZRevRank = 133,
ZInterStore = 134,
HRandField = 135,
}

fn get_two_word_command(first: &str, second: &str) -> Cmd {
Expand Down Expand Up @@ -289,6 +290,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::Touch => RequestType::Touch,
ProtobufRequestType::ZRevRank => RequestType::ZRevRank,
ProtobufRequestType::ZInterStore => RequestType::ZInterStore,
ProtobufRequestType::HRandField => RequestType::HRandField,
}
}
}
Expand Down Expand Up @@ -431,6 +433,7 @@ impl RequestType {
RequestType::Touch => Some(cmd("TOUCH")),
RequestType::ZRevRank => Some(cmd("ZREVRANK")),
RequestType::ZInterStore => Some(cmd("ZINTERSTORE")),
RequestType::HRandField => Some(cmd("HRANDFIELD")),
}
}
}
76 changes: 76 additions & 0 deletions python/python/glide/async_commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,82 @@ async def hkeys(self, key: str) -> List[str]:
"""
return cast(List[str], await self._execute_command(RequestType.Hkeys, [key]))

async def hrandfield(self, key: str) -> Optional[str]:
"""
Returns a random field name from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
Returns:
Optional[str]: A random field name from the hash stored at `key`.
If the hash does not exist or is empty, None will be returned.
Examples:
>>> await client.hrandfield("my_hash")
"field1" # A random field name stored in the hash "my_hash".
"""
return cast(
Optional[str], await self._execute_command(RequestType.HRandField, [key])
)

async def hrandfield_count(self, key: str, count: int) -> List[str]:
"""
Retrieves up to `count` random field names from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
count (int): The number of field names to return.
If `count` is positive, returns unique elements.
If negative, allows for duplicates.
Returns:
List[str]: A list of random field names from the hash.
If the hash does not exist or is empty, the response will be an empty list.
Examples:
>>> await client.hrandfield_count("my_hash", -3)
["field1", "field1", "field2"] # Non-distinct, random field names stored in the hash "my_hash".
>>> await client.hrandfield_count("non_existing_hash", 3)
[] # Empty list
"""
return cast(
List[str],
await self._execute_command(RequestType.HRandField, [key, str(count)]),
)

async def hrandfield_withvalues(self, key: str, count: int) -> List[List[str]]:
"""
Retrieves up to `count` random field names along with their values from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
count (int): The number of field names to return.
If `count` is positive, returns unique elements.
If negative, allows for duplicates.
Returns:
List[List[str]]: A list of `[field_name, value]` lists, where `field_name` 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 list.
Examples:
>>> await client.hrandfield_withvalues("my_hash", -3)
[["field1", "value1"], ["field1", "value1"], ["field2", "value2"]]
"""
return cast(
List[List[str]],
await self._execute_command(
RequestType.HRandField, [key, str(count), "WITHVALUES"]
),
)

async def lpush(self, key: str, elements: List[str]) -> int:
"""
Insert all the specified values at the head of the list stored at `key`.
Expand Down
54 changes: 54 additions & 0 deletions python/python/glide/async_commands/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,60 @@ def hkeys(self: TTransaction, key: str) -> TTransaction:
"""
return self.append_command(RequestType.Hkeys, [key])

def hrandfield(self: TTransaction, key: str) -> TTransaction:
"""
Returns a random field name from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
Command response:
Optional[str]: A random field name from the hash stored at `key`.
If the hash does not exist or is empty, None will be returned.
"""
return self.append_command(RequestType.HRandField, [key])

def hrandfield_count(self: TTransaction, key: str, count: int) -> TTransaction:
"""
Retrieves up to `count` random field names from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
count (int): The number of field names to return.
If `count` is positive, returns unique elements.
If negative, allows for duplicates.
Command response:
List[str]: A list of random field names from the hash.
If the hash does not exist or is empty, the response will be an empty list.
"""
return self.append_command(RequestType.HRandField, [key, str(count)])

def hrandfield_withvalues(self: TTransaction, key: str, count: int) -> TTransaction:
"""
Retrieves up to `count` random field names along with their values from the hash value stored at `key`.
See https://valkey.io/commands/hrandfield for more details.
Args:
key (str): The key of the hash.
count (int): The number of field names to return.
If `count` is positive, returns unique elements.
If negative, allows for duplicates.
Command response:
List[List[str]]: A list of `[field_name, value]` lists, where `field_name` 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 list.
"""
return self.append_command(
RequestType.HRandField, [key, str(count), "WITHVALUES"]
)

def lpush(self: TTransaction, key: str, elements: List[str]) -> TTransaction:
"""
Insert all the specified values at the head of the list stored at `key`.
Expand Down
Loading

0 comments on commit eb8a680

Please sign in to comment.