Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Python: add HRANDFIELD command #1334

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.clone()),
aaron-congo marked this conversation as resolved.
Show resolved Hide resolved
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]:
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
"""
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 @@ -608,6 +608,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
Loading