use crate::{
inputs::ObjectInput,
objects::{
hash::hash_object::{
HashObject, HashOperation, pick_k_random_indexes, pick_random_index, scan_operate_shared,
},
parse_utils::{equals_ignore_case, now_ticks, try_parse_with_infinity},
sortedset::sorted_set_object::ExpirationWithOption,
types::object_output::ObjectOutput,
},
resp::cmd_strings::RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER,
};
pub(crate) const RESP_ERR_HASH_VALUE_IS_NOT_INTEGER: &[u8] = b"ERR hash value is not an integer.";
pub(crate) const RESP_ERR_HASH_VALUE_IS_NOT_FLOAT: &[u8] = b"ERR hash value is not a float.";
pub(crate) const RESP_ERR_GENERIC_NAN_INFINITY: &[u8] = b"ERR value is NaN or Infinity";
use crate::resp::cmd_strings::{RESP_ERR_GENERIC_NAN_INFINITY_INCR, RESP_ERR_NOT_VALID_FLOAT};
#[inline]
fn arg<'a>(input: &ObjectInput, i: usize) -> &'a [u8] {
input.parse_state.get_arg_slice_by_ref(i).as_slice()
}
#[inline]
fn get_byte_span_from_input<'a>(input: &ObjectInput, index: usize) -> &'a [u8] {
arg(input, index)
}
fn num_utils_try_parse_long(v: &[u8]) -> Option<i64> {
let s = str::from_utf8(v).ok()?;
let digits = match s.as_bytes().first() {
Some(b'+') | Some(b'-') => &s[1..],
_ => s,
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
fn num_utils_try_parse_double(v: &[u8]) -> Option<f64> {
match v.len() {
3 if equals_ignore_case(v, b"inf") || equals_ignore_case(v, b"nan") => return None,
4 if equals_ignore_case(v, b"+inf")
|| equals_ignore_case(v, b"-inf")
|| equals_ignore_case(v, b"+nan")
|| equals_ignore_case(v, b"-nan") =>
{
return None;
}
_ => {}
}
let d = str::from_utf8(v).ok()?.parse::<f64>().ok()?;
if d.is_nan() || (d.is_infinite() && !v.iter().any(u8::is_ascii_digit)) {
return None;
}
Some(d)
}
#[inline]
fn format_double(value: f64) -> Vec<u8> {
ObjectOutput::format_double(value).into_bytes()
}
impl HashObject {
pub(crate) fn hash_get(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
resp_protocol_version: u8,
) {
let key = get_byte_span_from_input(input, 0);
match self.try_get_value(key) {
Some(hash_value) => output.write_bulk_string(hash_value),
None => output.write_null(resp_protocol_version),
}
output.result1 = 1;
}
pub(crate) fn hash_multiple_get(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
resp_protocol_version: u8,
) {
output.write_array_length(input.parse_state.count);
for i in 0..input.parse_state.count {
let key = get_byte_span_from_input(input, i);
match self.try_get_value(key) {
Some(hash_value) => output.write_bulk_string(hash_value),
None => output.write_null(resp_protocol_version),
}
}
output.result1 = input.parse_state.count as i64;
}
pub(crate) fn hash_get_all(&mut self, output: &mut ObjectOutput, resp_protocol_version: u8) {
write_map_length(output, self.count(), resp_protocol_version);
let is_expirable = self.has_expirable_items();
for (key, value) in self.hash.iter() {
if is_expirable && self.is_expired(key) {
continue;
}
output.write_bulk_string(key);
output.write_bulk_string(value);
}
}
pub(crate) fn hash_delete(&mut self, input: &ObjectInput, output: &mut ObjectOutput) {
let mut removed = 0_i64;
for i in 0..input.parse_state.count {
let key = get_byte_span_from_input(input, i);
if self.remove(key).is_some() {
removed += 1;
}
}
output.result1 = removed;
}
pub(crate) fn hash_length(&mut self, output: &mut ObjectOutput) {
output.result1 = self.count() as i64;
}
pub(crate) fn hash_str_length(&mut self, input: &ObjectInput, output: &mut ObjectOutput) {
let key = get_byte_span_from_input(input, 0);
output.result1 = match self.try_get_value(key) {
Some(hash_value) => hash_value.len() as i64,
None => 0,
};
}
pub(crate) fn hash_exists(&mut self, input: &ObjectInput, output: &mut ObjectOutput) {
let field = get_byte_span_from_input(input, 0);
output.result1 = i64::from(self.contains_key(field));
}
pub(crate) fn hash_random_field(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
resp_protocol_version: u8,
) {
let mut count_parameter = (input.arg1 >> 2) as i64;
let with_values = (input.arg1 & 1) == 1;
let included_count = ((input.arg1 >> 1) & 1) == 1;
let seed = input.arg2;
let mut count_done = 0_i64;
if included_count {
let count = self.count();
if count == 0 {
output.write_empty_array();
output.result1 = 0;
return;
}
if count_parameter > 0 && count_parameter > count as i64 {
count_parameter = count as i64;
}
let index_count = count_parameter.unsigned_abs() as usize;
let indexes = pick_k_random_indexes(count, index_count, seed, count_parameter > 0);
output.write_array_length(if with_values && resp_protocol_version == 2 {
index_count * 2
} else {
index_count
});
for index in indexes {
let Some((key, value)) = self.element_at(index) else {
continue;
};
if resp_protocol_version >= 3 && with_values {
output.write_array_length(2);
}
output.write_bulk_string(&key);
if with_values {
output.write_bulk_string(&value);
}
count_done += 1;
}
} else {
let count = self.count();
if count == 0 {
output.write_null(resp_protocol_version);
output.result1 = 0;
return;
}
let index = pick_random_index(count, seed);
if let Some((key, _)) = self.element_at(index) {
output.write_bulk_string(&key);
}
count_done = 1;
}
output.result1 = count_done;
}
pub(crate) fn hash_set(&mut self, input: &ObjectInput, output: &mut ObjectOutput) {
self.delete_expired_items();
let mut set = 0_i64;
let hash_op = input.header.sub_id();
let mut i = 0;
while i < input.parse_state.count {
let key = get_byte_span_from_input(input, i);
let value = arg(input, i + 1);
match self.hash.get(key) {
None => {
self.hash.insert(key.to_vec(), value.to_vec());
self.update_size(key, value, true);
set += 1;
}
Some(old_value) => {
let old_value = old_value.clone();
if matches!(
HashOperation::try_from(hash_op),
Ok(HashOperation::Hset) | Ok(HashOperation::Hmset)
) {
self.heap_memory_size +=
value.len().div_ceil(8) as i64 * 8 - old_value.len().div_ceil(8) as i64 * 8;
self.hash.insert(key.to_vec(), value.to_vec());
if self.has_expirable_items()
&& self
.expiration_times
.as_mut()
.unwrap()
.remove(key)
.is_some()
{
self.heap_memory_size -= 16 + 16;
self.cleanup_expiration_structures_if_empty();
}
} else {
let _ = old_value;
}
}
}
i += 2;
}
output.result1 = set;
}
pub(crate) fn hash_collect(&mut self, output: &mut ObjectOutput) {
self.delete_expired_items();
output.result1 = 1;
}
pub(crate) fn hash_get_keys_or_values(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
let count = self.count();
let Ok(op) = HashOperation::try_from(input.header.sub_id()) else {
return;
};
output.write_array_length(count);
let is_expirable = self.has_expirable_items();
let mut written = 0_i64;
for (key, value) in self.hash.iter() {
if is_expirable && self.is_expired(key) {
continue;
}
if op == HashOperation::Hkeys {
output.write_bulk_string(key);
} else {
output.write_bulk_string(value);
}
written += 1;
}
output.result1 = written;
}
pub(crate) fn hash_increment(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
output.result1 = i32::MIN as i64;
let key = get_byte_span_from_input(input, 0);
let incr_slice = arg(input, 1);
let Some(incr) = num_utils_try_parse_long(incr_slice) else {
output.write_error(RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER.as_bytes());
return;
};
self.delete_expired_items();
match self.hash.get(key).cloned() {
None => {
self.add(key, incr_slice.to_vec());
write_integer_from_bytes(output, incr_slice);
}
Some(hash_value) => {
let Some(result) = num_utils_try_parse_long(&hash_value) else {
output.write_error(RESP_ERR_HASH_VALUE_IS_NOT_INTEGER);
return;
};
let result = result.wrapping_add(incr);
let formatted_value = format_i64(result);
self.replace_value(key, &hash_value, &formatted_value);
write_integer_from_bytes(output, &formatted_value);
}
}
output.result1 = 1;
}
pub(crate) fn hash_increment_float(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
output.result1 = i32::MIN as i64;
let key = get_byte_span_from_input(input, 0);
let incr_slice = arg(input, 1);
let Some(incr) = num_utils_try_parse_double(incr_slice) else {
output.write_error(RESP_ERR_NOT_VALID_FLOAT.as_bytes());
return;
};
if incr.is_infinite() {
output.write_error(RESP_ERR_GENERIC_NAN_INFINITY);
return;
}
self.delete_expired_items();
match self.hash.get(key).cloned() {
None => {
self.add(key, incr_slice.to_vec());
output.write_bulk_string(incr_slice);
}
Some(hash_value) => {
let Some(result) = try_parse_with_infinity(&hash_value) else {
output.write_error(RESP_ERR_HASH_VALUE_IS_NOT_FLOAT);
return;
};
if result.is_infinite() {
output.write_error(RESP_ERR_GENERIC_NAN_INFINITY_INCR.as_bytes());
return;
}
let result = result + incr;
let formatted_value = format_double(result);
self.replace_value(key, &hash_value, &formatted_value);
output.write_bulk_string(&formatted_value);
}
}
output.result1 = 1;
}
pub(crate) fn hash_expire(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
self.delete_expired_items();
let expiration_with_option = ExpirationWithOption::from_word_head_tail(input.arg1, input.arg2);
output.write_array_length(input.parse_state.count);
for i in 0..input.parse_state.count {
let result = self.set_expiration(
get_byte_span_from_input(input, i),
expiration_with_option.expiration_time_in_ticks(),
expiration_with_option.expire_option(),
);
output.write_int64(i64::from(result as i32));
}
output.result1 = input.parse_state.count as i64;
}
pub(crate) fn hash_time_to_live(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
self.delete_expired_items();
let is_milliseconds = input.arg1 == 1;
let is_timestamp = input.arg2 == 1;
let num_fields = input.parse_state.count;
output.write_array_length(num_fields);
for i in 0..num_fields {
let mut result = self.get_expiration(get_byte_span_from_input(input, i));
if result >= 0 {
if is_timestamp && is_milliseconds {
result = unix_time_in_milliseconds_from_ticks(result);
} else if is_timestamp && !is_milliseconds {
result = unix_time_in_seconds_from_ticks(result);
} else if !is_timestamp && is_milliseconds {
result = milliseconds_from_diff_utc_now_ticks(result);
} else if !is_timestamp && !is_milliseconds {
result = seconds_from_diff_utc_now_ticks(result);
}
}
output.write_int64(result);
}
output.result1 = num_fields as i64;
}
pub(crate) fn hash_persist(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
self.delete_expired_items();
let num_fields = input.parse_state.count;
output.write_array_length(num_fields);
for i in 0..num_fields {
let result = self.persist(get_byte_span_from_input(input, i));
output.write_int64(i64::from(result));
}
output.result1 = num_fields as i64;
}
fn replace_value(&mut self, key: &[u8], old_value: &[u8], new_value: &[u8]) {
self.heap_memory_size +=
new_value.len().div_ceil(8) as i64 * 8 - old_value.len().div_ceil(8) as i64 * 8;
self.hash.insert(key.to_vec(), new_value.to_vec());
}
pub(crate) fn scan_operate(
&mut self,
input: &ObjectInput,
output: &mut ObjectOutput,
_resp_protocol_version: u8,
) {
scan_operate_shared(input, output, |cursor, count, pattern, is_no_value| {
self.scan(cursor, count, pattern, is_no_value)
});
}
}
fn write_map_length(output: &mut ObjectOutput, len: usize, resp_protocol_version: u8) {
if resp_protocol_version >= 3 {
output.payload.push(b'%');
let mut buf = itoa::Buffer::new();
output.payload.extend_from_slice(buf.format(len).as_bytes());
output.payload.extend_from_slice(b"\r\n");
} else {
output.write_array_length(len * 2);
}
}
#[inline]
fn write_integer_from_bytes(output: &mut ObjectOutput, value: &[u8]) {
output.payload.push(b':');
output.payload.extend_from_slice(value);
output.payload.extend_from_slice(b"\r\n");
}
#[inline]
fn format_i64(value: i64) -> Vec<u8> {
let mut buf = itoa::Buffer::new();
buf.format(value).as_bytes().to_vec()
}
#[inline]
fn unix_time_in_milliseconds_from_ticks(ticks: i64) -> i64 {
const UNIX_EPOCH_TICKS: i64 = 621_355_968_000_000_000;
if ticks <= 0 {
return -1;
}
(ticks - UNIX_EPOCH_TICKS) / 10_000
}
#[inline]
fn unix_time_in_seconds_from_ticks(ticks: i64) -> i64 {
const UNIX_EPOCH_TICKS: i64 = 621_355_968_000_000_000;
if ticks <= 0 {
return -1;
}
(ticks - UNIX_EPOCH_TICKS) / 10_000_000
}
#[inline]
fn milliseconds_from_diff_utc_now_ticks(ticks: i64) -> i64 {
let diff = ticks - now_ticks();
if diff > 0 { diff / 10_000 } else { -1 }
}
#[inline]
fn seconds_from_diff_utc_now_ticks(ticks: i64) -> i64 {
let diff = ticks - now_ticks();
if diff > 0 {
(diff + 5_000_000) / 10_000_000
} else {
-1
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
arg_slice::ArgSlice,
input_header::RespInputHeader,
objects::hash::hash_object::{ExpireOption, HashOperation},
session_parse_state::SessionParseState,
types::{GarnetObjectType, RespInputFlags},
};
fn make_input(
op: HashOperation,
args: &[&[u8]],
arg1: i32,
arg2: i32,
) -> (ObjectInput, Vec<Vec<u8>>) {
let backing: Vec<Vec<u8>> = args.iter().map(|a| a.to_vec()).collect();
let slices: Vec<ArgSlice> = backing
.iter()
.map(|b| ArgSlice::new(b.as_ptr(), b.len()))
.collect();
let mut parse_state = SessionParseState::new();
parse_state.initialize_with_args(&slices);
let mut header =
RespInputHeader::new_with_type(GarnetObjectType::Hash, RespInputFlags::empty());
header.set_sub_id(op as u8);
(
ObjectInput::new_with_state(header, &mut parse_state, arg1, arg2),
backing,
)
}
fn seed(obj: &mut HashObject, fields: &[(&str, &str)]) {
for (k, v) in fields {
obj
.hash
.insert(k.as_bytes().to_vec(), v.as_bytes().to_vec());
}
}
fn payload_str(out: &ObjectOutput) -> String {
String::from_utf8_lossy(&out.payload).into_owned()
}
fn parse_bulk_items(frame: &[u8]) -> Vec<Vec<u8>> {
let mut items = Vec::new();
let mut pos = 0;
while pos < frame.len() {
if frame[pos] != b'$' {
pos += 1;
continue;
}
let Some(line_end) = frame[pos..]
.iter()
.position(|&b| b == b'\n')
.map(|p| p + pos)
else {
break;
};
let Ok(len) = str::from_utf8(&frame[pos + 1..line_end - 1])
.unwrap_or("")
.parse::<usize>()
else {
break;
};
let start = line_end + 1;
items.push(frame[start..start + len].to_vec());
pos = start + len + 2;
}
items
}
#[test]
fn read_ops() {
let mut obj = HashObject::new();
seed(&mut obj, &[("a", "1"), ("b", "22")]);
let (input, _b) = make_input(HashOperation::Hget, &[b"a"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_get(&input, &mut out, 2);
assert_eq!(out.payload, b"$1\r\n1\r\n");
assert_eq!(out.result1, 1);
let (input, _b) = make_input(HashOperation::Hmget, &[b"a", b"nx", b"b"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_multiple_get(&input, &mut out, 2);
assert_eq!(out.payload, b"*3\r\n$1\r\n1\r\n$-1\r\n$2\r\n22\r\n");
assert_eq!(out.result1, 3);
let (_input, _b) = make_input(HashOperation::Hgetall, &[], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_get_all(&mut out, 2);
let payload = payload_str(&out);
assert!(payload.starts_with("*4\r\n"), "{payload}");
for tok in ["1", "22", "a", "b"] {
assert!(
payload.contains(&format!("${}\r\n{}\r\n", tok.len(), tok)),
"{payload}"
);
}
let (input, _b) = make_input(HashOperation::Hkeys, &[], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_get_keys_or_values(&input, &mut out, 2);
let mut keys = parse_bulk_items(&out.payload);
keys.sort();
assert_eq!(keys, vec![b"a".to_vec(), b"b".to_vec()]);
assert_eq!(out.result1, 2);
let (input, _b) = make_input(HashOperation::Hvals, &[], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_get_keys_or_values(&input, &mut out, 2);
let mut vals = parse_bulk_items(&out.payload);
vals.sort();
assert_eq!(vals, vec![b"1".to_vec(), b"22".to_vec()]);
let (_input, _b) = make_input(HashOperation::Hlen, &[], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_length(&mut out);
assert_eq!(out.result1, 2);
let (input, _b) = make_input(HashOperation::Hstrlen, &[b"b"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_str_length(&input, &mut out);
assert_eq!(out.result1, 2);
let (input, _b) = make_input(HashOperation::Hstrlen, &[b"nx"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_str_length(&input, &mut out);
assert_eq!(out.result1, 0);
let (input, _b) = make_input(HashOperation::Hexists, &[b"a"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_exists(&input, &mut out);
assert_eq!(out.result1, 1);
let (input, _b) = make_input(HashOperation::Hexists, &[b"nx"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_exists(&input, &mut out);
assert_eq!(out.result1, 0);
}
#[test]
fn set_ops() {
let mut obj = HashObject::new();
let (input, _b) = make_input(HashOperation::Hset, &[b"f1", b"v1", b"f2", b"v2"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 2);
let (input, _b) = make_input(HashOperation::Hset, &[b"f1", b"v9"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 0);
assert_eq!(obj.try_get_value(b"f1"), Some(&b"v9".to_vec()));
let (input, _b) = make_input(HashOperation::Hmset, &[b"f3", b"v3"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 1);
let (input, _b) = make_input(HashOperation::Hsetnx, &[b"f1", b"other"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 0);
assert_eq!(obj.try_get_value(b"f1"), Some(&b"v9".to_vec()));
let (input, _b) = make_input(HashOperation::Hsetnx, &[b"f4", b"v4"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 1);
let future = now_ticks() + 10_000_000;
let _ = obj.set_expiration(b"f2", future, ExpireOption::NONE);
let (input, _b) = make_input(HashOperation::Hset, &[b"f2", b"vv"], 0, 0);
obj.hash_set(&input, &mut ObjectOutput::new());
assert_eq!(obj.get_expiration(b"f2"), -1);
}
#[test]
fn delete_and_collect() {
let mut obj = HashObject::new();
seed(&mut obj, &[("a", "1"), ("b", "2")]);
let (input, _b) = make_input(HashOperation::Hdel, &[b"a", b"nx"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_delete(&input, &mut out);
assert_eq!(out.result1, 1);
assert_eq!(obj.count(), 1);
let (_input, _b) = make_input(HashOperation::Hcollect, &[], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_collect(&mut out);
assert_eq!(out.result1, 1);
}
#[test]
fn incr_int() {
let mut obj = HashObject::new();
let (input, _b) = make_input(HashOperation::Hincrby, &[b"f", b"10"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":10\r\n");
assert_eq!(out.result1, 1);
assert_eq!(obj.try_get_value(b"f"), Some(&b"10".to_vec()));
let (input, _b) = make_input(HashOperation::Hincrby, &[b"f", b"-3"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":7\r\n");
assert_eq!(obj.try_get_value(b"f"), Some(&b"7".to_vec()));
seed(&mut obj, &[("s", "abc")]);
let (input, _b) = make_input(HashOperation::Hincrby, &[b"s", b"1"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR hash value is not an integer.\r\n");
let (input, _b) = make_input(HashOperation::Hincrby, &[b"f", b"1.5"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(
out.payload,
b"-ERR value is not an integer or out of range.\r\n"
);
let (input, _b) = make_input(HashOperation::Hincrby, &[b"f", b"+007"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":14\r\n");
let (input, _b) = make_input(HashOperation::Hincrby, &[b"g", b"+007"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":+007\r\n");
assert_eq!(obj.try_get_value(b"g"), Some(&b"+007".to_vec()));
}
#[test]
fn incr_float() {
let mut obj = HashObject::new();
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"f", b"10.5"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"$4\r\n10.5\r\n");
assert_eq!(obj.try_get_value(b"f"), Some(&b"10.5".to_vec()));
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"f", b"0.1"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"$4\r\n10.6\r\n", "{}", payload_str(&out));
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"f", b"inf"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR value is not a valid float\r\n");
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"f", b"1e400"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR value is NaN or Infinity\r\n");
seed(&mut obj, &[("s", "abc")]);
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"s", b"1.5"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR hash value is not a float.\r\n");
seed(&mut obj, &[("i", "inf")]);
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"i", b"1.5"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(
out.payload,
b"-ERR increment would produce NaN or Infinity\r\n"
);
seed(&mut obj, &[("w", "Infinity")]);
let (input, _b) = make_input(HashOperation::Hincrbyfloat, &[b"w", b"1.5"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment_float(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR hash value is not a float.\r\n");
}
#[test]
fn expire_family() {
let mut obj = HashObject::new();
seed(&mut obj, &[("a", "1"), ("b", "2")]);
let exp = now_ticks() + 1_000_000;
let e = ExpirationWithOption::new(exp, ExpireOption::NONE);
let (input, _b) = make_input(
HashOperation::Hexpire,
&[b"a", b"zz"],
(e.word() >> 32) as i32,
e.word() as i32,
);
let mut out = ObjectOutput::new();
obj.hash_expire(&input, &mut out, 2);
assert_eq!(out.payload, b"*2\r\n:1\r\n:-2\r\n");
assert_eq!(out.result1, 2);
let (input, _b) = make_input(HashOperation::Httl, &[b"a"], 1, 0);
let mut out = ObjectOutput::new();
obj.hash_time_to_live(&input, &mut out, 2);
let payload = payload_str(&out);
let ttl: i64 = payload
.lines()
.nth(1)
.and_then(|l| l.trim_start_matches(':').parse().ok())
.unwrap_or(-999);
assert!((90..=100).contains(&ttl), "{payload}");
let (input, _b) = make_input(HashOperation::Httl, &[b"a"], 0, 1);
let mut out = ObjectOutput::new();
obj.hash_time_to_live(&input, &mut out, 2);
let payload = payload_str(&out);
let ts: i64 = payload
.lines()
.nth(1)
.and_then(|l| l.trim_start_matches(':').parse().ok())
.unwrap_or(-999);
const UNIX_EPOCH_TICKS: i64 = 621_355_968_000_000_000;
let expected = (exp - UNIX_EPOCH_TICKS) / 10_000_000;
assert!((ts - expected).abs() <= 1, "{payload} vs {expected}");
let (input, _b) = make_input(HashOperation::Hpersist, &[b"a", b"b"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_persist(&input, &mut out, 2);
assert_eq!(out.payload, b"*2\r\n:1\r\n:-1\r\n");
assert!(!obj.has_expirable_items());
}
#[test]
fn random_field() {
let mut obj = HashObject::new();
seed(&mut obj, &[("a", "1"), ("b", "2"), ("c", "3")]);
let (input, _b) = make_input(HashOperation::Hrandfield, &[], 0, 7);
let mut out = ObjectOutput::new();
obj.hash_random_field(&input, &mut out, 2);
assert!(out.payload.starts_with(b"$1\r\n"), "{}", payload_str(&out));
assert_eq!(out.result1, 1);
let (input, _b) = make_input(HashOperation::Hrandfield, &[], ((2 << 1) | 1) << 1, 42);
let mut out = ObjectOutput::new();
obj.hash_random_field(&input, &mut out, 2);
assert!(out.payload.starts_with(b"*2\r\n"), "{}", payload_str(&out));
let (input, _b) = make_input(
HashOperation::Hrandfield,
&[],
(((2 << 1) | 1) << 1) | 1,
42,
);
let mut out = ObjectOutput::new();
obj.hash_random_field(&input, &mut out, 2);
assert!(out.payload.starts_with(b"*4\r\n"), "{}", payload_str(&out));
let (input, _b) = make_input(HashOperation::Hrandfield, &[], ((5 << 1) | 1) << 1, 42);
let mut out = ObjectOutput::new();
obj.hash_random_field(&input, &mut out, 2);
assert!(out.payload.starts_with(b"*3\r\n"), "{}", payload_str(&out));
let mut empty = HashObject::new();
let (input, _b) = make_input(HashOperation::Hrandfield, &[], ((1 << 1) | 1) << 1, 1);
let mut out = ObjectOutput::new();
empty.hash_random_field(&input, &mut out, 2);
assert_eq!(out.payload, b"*0\r\n");
assert_eq!(out.result1, 0);
let (input, _b) = make_input(HashOperation::Hrandfield, &[], 0, 1);
let mut out = ObjectOutput::new();
empty.hash_random_field(&input, &mut out, 2);
assert_eq!(out.payload, b"$-1\r\n");
}
#[test]
fn scan_flow() {
let mut obj = HashObject::new();
seed(&mut obj, &[("one", "1"), ("two", "2"), ("three", "3")]);
let (input, _b) = make_input(HashOperation::Hscan, &[b"0", b"MATCH", b"t*"], 0, 0);
let mut out = ObjectOutput::new();
assert!(obj.operate(&input, &mut out, 2));
let payload = payload_str(&out);
assert!(payload.starts_with("*2\r\n$1\r\n0\r\n*4\r\n"), "{payload}");
for (field, value) in [("two", "2"), ("three", "3")] {
assert!(
payload.contains(&format!("${}\r\n{}\r\n", field.len(), field)),
"{payload}"
);
assert!(
payload.contains(&format!("$1\r\n{}\r\n", value)),
"{payload}"
);
}
let (input, _b) = make_input(HashOperation::Hscan, &[b"0", b"NOVALUES"], 0, 0);
let mut out = ObjectOutput::new();
obj.scan_operate(&input, &mut out, 2);
let payload = payload_str(&out);
assert!(payload.starts_with("*2\r\n$1\r\n0\r\n*3\r\n"), "{payload}");
let (input, _b) = make_input(HashOperation::Hscan, &[b"-1"], 0, 0);
let mut out = ObjectOutput::new();
obj.scan_operate(&input, &mut out, 2);
assert_eq!(out.payload, b"-ERR invalid cursor\r\n");
let (input, _b) = make_input(HashOperation::Hget, &[b"a"], 0, 0);
let mut input = input;
input.header.data[0] = GarnetObjectType::List as u8;
let mut out = ObjectOutput::new();
obj.operate(&input, &mut out, 2);
assert!(out.has_wrong_type());
}
#[test]
fn operate_dispatch_smoke() {
let mut obj = HashObject::new();
let (input, _b) = make_input(HashOperation::Hset, &[b"f", b"v"], 0, 0);
let mut out = ObjectOutput::new();
assert!(obj.operate(&input, &mut out, 2));
assert_eq!(out.result1, 1);
let (input, _b) = make_input(HashOperation::Hdel, &[b"f"], 0, 0);
let mut out = ObjectOutput::new();
obj.operate(&input, &mut out, 2);
assert!(out.has_remove_key());
}
#[test]
fn incr_overflow_and_reaccount() {
let mut obj = HashObject::new();
seed(&mut obj, &[("m", "9223372036854775807")]);
let (input, _b) = make_input(HashOperation::Hincrby, &[b"m", b"1"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":-9223372036854775808\r\n");
assert_eq!(
obj.try_get_value(b"m"),
Some(&b"-9223372036854775808".to_vec())
);
let mut obj = HashObject::new();
seed(&mut obj, &[("k", "0123456789ABCDEF0123456789")]);
let before = obj.heap_memory_size;
let (input, _b) = make_input(HashOperation::Hset, &[b"k", b"xy"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_set(&input, &mut out);
assert_eq!(out.result1, 0);
assert_eq!(obj.try_get_value(b"k"), Some(&b"xy".to_vec()));
assert_eq!(obj.heap_memory_size, before - 24);
let mut obj = HashObject::new();
seed(&mut obj, &[("n", "9223372036854775807")]);
let before = obj.heap_memory_size;
let (input, _b) = make_input(HashOperation::Hincrby, &[b"n", b"-1"], 0, 0);
let mut out = ObjectOutput::new();
obj.hash_increment(&input, &mut out, 2);
assert_eq!(out.payload, b":9223372036854775806\r\n");
assert_eq!(obj.heap_memory_size, before);
}
#[test]
fn parse_long_matrix() {
assert_eq!(num_utils_try_parse_long(b"42"), Some(42));
assert_eq!(num_utils_try_parse_long(b"-7"), Some(-7));
assert_eq!(num_utils_try_parse_long(b"+7"), Some(7));
assert_eq!(num_utils_try_parse_long(b"007"), Some(7));
assert_eq!(num_utils_try_parse_long(b""), None);
assert_eq!(num_utils_try_parse_long(b"-"), None);
assert_eq!(num_utils_try_parse_long(b"1.5"), None);
assert_eq!(num_utils_try_parse_long(b"1x"), None);
assert_eq!(
num_utils_try_parse_long(i64::MIN.to_string().as_bytes()),
Some(i64::MIN)
);
assert_eq!(num_utils_try_parse_long(b"99999999999999999999"), None);
}
#[test]
fn parse_double_matrix() {
assert_eq!(num_utils_try_parse_double(b"1.5"), Some(1.5));
assert_eq!(num_utils_try_parse_double(b"1e400"), Some(f64::INFINITY));
assert_eq!(
num_utils_try_parse_double(b"-1e400"),
Some(f64::NEG_INFINITY)
);
assert_eq!(num_utils_try_parse_double(b"inf"), None);
assert_eq!(num_utils_try_parse_double(b"INF"), None);
assert_eq!(num_utils_try_parse_double(b"-inf"), None);
assert_eq!(num_utils_try_parse_double(b"nan"), None);
assert_eq!(num_utils_try_parse_double(b"Infinity"), None);
assert_eq!(num_utils_try_parse_double(b""), None);
assert_eq!(num_utils_try_parse_double(b"1.5x"), None);
}
}