diff --git a/crates/ember-core/src/keyspace/hash.rs b/crates/ember-core/src/keyspace/hash.rs index ed8fe440..ac52d52a 100644 --- a/crates/ember-core/src/keyspace/hash.rs +++ b/crates/ember-core/src/keyspace/hash.rs @@ -299,6 +299,84 @@ impl Keyspace { _ => Err(WrongType), } } + + /// Returns random field(s) from a hash. + /// + /// - `count = None`: return one random field (no value, even if `with_values` is set) + /// - `count > 0`: return up to count distinct fields + /// - `count < 0`: return |count| fields, allowing duplicates + /// + /// If `with_values` is true and count is `Some`, returns interleaved `(field, Some(value))` + /// pairs. When `count` is `None`, only the field name is returned as `(field, None)`. + pub fn hrandfield( + &mut self, + key: &str, + count: Option, + with_values: bool, + ) -> Result)>, WrongType> { + let Some(entry) = self.get_live_entry(key) else { + return Ok(vec![]); + }; + let Value::Hash(ref hash) = entry.value else { + return Err(WrongType); + }; + if hash.is_empty() { + return Ok(vec![]); + } + + // collect into a vec for indexed random access + let fields: Vec<(&str, &[u8])> = hash.iter().collect(); + let mut rng = rand::rng(); + + let result = match count { + None => { + // single random field — no value even with with_values + use rand::seq::IteratorRandom; + fields + .iter() + .choose(&mut rng) + .map(|(f, _)| ((*f).to_owned(), None)) + .into_iter() + .collect() + } + Some(n) if n > 0 => { + use rand::seq::IteratorRandom; + let n = (n as usize).min(fields.len()); + fields + .iter() + .choose_multiple(&mut rng, n) + .into_iter() + .map(|(f, v)| { + let val = if with_values { + Some(Bytes::copy_from_slice(v)) + } else { + None + }; + ((*f).to_owned(), val) + }) + .collect() + } + Some(n) => { + // negative count: allow duplicates, return |n| entries + use rand::Rng; + let n = n.unsigned_abs() as usize; + (0..n) + .map(|_| { + let idx = rng.random_range(0..fields.len()); + let (f, v) = fields[idx]; + let val = if with_values { + Some(Bytes::copy_from_slice(v)) + } else { + None + }; + (f.to_owned(), val) + }) + .collect() + } + }; + + Ok(result) + } } #[cfg(test)] @@ -664,4 +742,106 @@ mod tests { assert_eq!(ks.len(), 0); assert!(!ks.exists("h")); } + + // --- hrandfield --- + + #[test] + fn hrandfield_no_count_returns_single_field() { + let mut ks = Keyspace::new(); + ks.hset( + "h", + &[ + ("a".into(), Bytes::from("1")), + ("b".into(), Bytes::from("2")), + ("c".into(), Bytes::from("3")), + ], + ) + .unwrap(); + let result = ks.hrandfield("h", None, false).unwrap(); + assert_eq!(result.len(), 1); + assert!(["a", "b", "c"].contains(&result[0].0.as_str())); + // no count means no value, even with_values would be ignored + assert!(result[0].1.is_none()); + } + + #[test] + fn hrandfield_positive_count_distinct() { + let mut ks = Keyspace::new(); + ks.hset( + "h", + &[ + ("a".into(), Bytes::from("1")), + ("b".into(), Bytes::from("2")), + ("c".into(), Bytes::from("3")), + ], + ) + .unwrap(); + let result = ks.hrandfield("h", Some(2), false).unwrap(); + assert_eq!(result.len(), 2); + // all returned should be valid fields + for (f, v) in &result { + assert!(["a", "b", "c"].contains(&f.as_str())); + assert!(v.is_none()); + } + // distinct + let unique: std::collections::HashSet<_> = result.iter().map(|(f, _)| f).collect(); + assert_eq!(unique.len(), 2); + } + + #[test] + fn hrandfield_positive_count_capped_at_hash_size() { + let mut ks = Keyspace::new(); + ks.hset( + "h", + &[ + ("a".into(), Bytes::from("1")), + ("b".into(), Bytes::from("2")), + ], + ) + .unwrap(); + let result = ks.hrandfield("h", Some(10), false).unwrap(); + assert_eq!(result.len(), 2); + } + + #[test] + fn hrandfield_negative_count_allows_duplicates() { + let mut ks = Keyspace::new(); + ks.hset("h", &[("only".into(), Bytes::from("v"))]).unwrap(); + let result = ks.hrandfield("h", Some(-5), false).unwrap(); + assert_eq!(result.len(), 5); + assert!(result.iter().all(|(f, _)| f == "only")); + } + + #[test] + fn hrandfield_with_values() { + let mut ks = Keyspace::new(); + ks.hset( + "h", + &[ + ("field".into(), Bytes::from("value")), + ("other".into(), Bytes::from("data")), + ], + ) + .unwrap(); + let result = ks.hrandfield("h", Some(2), true).unwrap(); + assert_eq!(result.len(), 2); + for (f, v) in &result { + assert!(["field", "other"].contains(&f.as_str())); + assert!(v.is_some()); + } + } + + #[test] + fn hrandfield_missing_key_returns_empty() { + let mut ks = Keyspace::new(); + assert!(ks.hrandfield("missing", None, false).unwrap().is_empty()); + assert!(ks.hrandfield("missing", Some(5), true).unwrap().is_empty()); + } + + #[test] + fn hrandfield_wrong_type_returns_error() { + let mut ks = Keyspace::new(); + ks.set("s".into(), Bytes::from("val"), None, false, false); + assert!(ks.hrandfield("s", None, false).is_err()); + } } diff --git a/crates/ember-core/src/keyspace/zset.rs b/crates/ember-core/src/keyspace/zset.rs index b47ddcc0..dde4ddb8 100644 --- a/crates/ember-core/src/keyspace/zset.rs +++ b/crates/ember-core/src/keyspace/zset.rs @@ -551,6 +551,76 @@ impl Keyspace { }); Ok(result) } + + /// Returns random member(s) from a sorted set. + /// + /// - `count = None`: return one random member as a single string (no score) + /// - `count > 0`: return up to count distinct members + /// - `count < 0`: return |count| members, allowing duplicates + /// + /// If `with_scores` is true and count is `Some`, returns `(member, Some(score))` pairs. + /// When `count` is `None`, the score field is always `None`. + pub fn zrandmember( + &mut self, + key: &str, + count: Option, + with_scores: bool, + ) -> Result)>, WrongType> { + let Some(entry) = self.get_live_entry(key) else { + return Ok(vec![]); + }; + let Value::SortedSet(ref zset) = entry.value else { + return Err(WrongType); + }; + if zset.is_empty() { + return Ok(vec![]); + } + + // collect into a vec for indexed random access + let members: Vec<(&str, f64)> = zset.iter().collect(); + let mut rng = rand::rng(); + + let result = match count { + None => { + // single random member — no score even if with_scores is set + use rand::seq::IteratorRandom; + members + .iter() + .choose(&mut rng) + .map(|(m, _)| ((*m).to_owned(), None)) + .into_iter() + .collect() + } + Some(n) if n > 0 => { + use rand::seq::IteratorRandom; + let n = (n as usize).min(members.len()); + members + .iter() + .choose_multiple(&mut rng, n) + .into_iter() + .map(|(m, s)| { + let score = if with_scores { Some(*s) } else { None }; + ((*m).to_owned(), score) + }) + .collect() + } + Some(n) => { + // negative count: allow duplicates, return |n| entries + use rand::Rng; + let n = n.unsigned_abs() as usize; + (0..n) + .map(|_| { + let idx = rng.random_range(0..members.len()); + let (m, s) = members[idx]; + let score = if with_scores { Some(s) } else { None }; + (m.to_owned(), score) + }) + .collect() + } + }; + + Ok(result) + } } #[cfg(test)] @@ -1055,4 +1125,96 @@ mod tests { let keys = vec!["s".to_owned()]; assert!(ks.zdiff(&keys).is_err()); } + + // --- zrandmember --- + + #[test] + fn zrandmember_no_count_returns_single_member() { + let mut ks = Keyspace::new(); + ks.zadd( + "z", + &[(1.0, "a".into()), (2.0, "b".into()), (3.0, "c".into())], + &ZAddFlags::default(), + ) + .unwrap(); + let result = ks.zrandmember("z", None, false).unwrap(); + assert_eq!(result.len(), 1); + assert!(["a", "b", "c"].contains(&result[0].0.as_str())); + // no count means no score + assert!(result[0].1.is_none()); + } + + #[test] + fn zrandmember_positive_count_distinct() { + let mut ks = Keyspace::new(); + ks.zadd( + "z", + &[(1.0, "a".into()), (2.0, "b".into()), (3.0, "c".into())], + &ZAddFlags::default(), + ) + .unwrap(); + let result = ks.zrandmember("z", Some(2), false).unwrap(); + assert_eq!(result.len(), 2); + for (m, s) in &result { + assert!(["a", "b", "c"].contains(&m.as_str())); + assert!(s.is_none()); + } + // distinct + let unique: std::collections::HashSet<_> = result.iter().map(|(m, _)| m).collect(); + assert_eq!(unique.len(), 2); + } + + #[test] + fn zrandmember_positive_count_capped_at_set_size() { + let mut ks = Keyspace::new(); + ks.zadd( + "z", + &[(1.0, "a".into()), (2.0, "b".into())], + &ZAddFlags::default(), + ) + .unwrap(); + let result = ks.zrandmember("z", Some(10), false).unwrap(); + assert_eq!(result.len(), 2); + } + + #[test] + fn zrandmember_negative_count_allows_duplicates() { + let mut ks = Keyspace::new(); + ks.zadd("z", &[(1.0, "only".into())], &ZAddFlags::default()) + .unwrap(); + let result = ks.zrandmember("z", Some(-5), false).unwrap(); + assert_eq!(result.len(), 5); + assert!(result.iter().all(|(m, _)| m == "only")); + } + + #[test] + fn zrandmember_with_scores() { + let mut ks = Keyspace::new(); + ks.zadd( + "z", + &[(1.0, "a".into()), (2.0, "b".into())], + &ZAddFlags::default(), + ) + .unwrap(); + let result = ks.zrandmember("z", Some(2), true).unwrap(); + assert_eq!(result.len(), 2); + for (m, s) in &result { + assert!(["a", "b"].contains(&m.as_str())); + assert!(s.is_some()); + } + } + + #[test] + fn zrandmember_missing_key_returns_empty() { + let mut ks = Keyspace::new(); + assert!(ks.zrandmember("missing", None, false).unwrap().is_empty()); + assert!(ks.zrandmember("missing", Some(5), true).unwrap().is_empty()); + } + + #[test] + fn zrandmember_wrong_type_returns_error() { + let mut ks = Keyspace::new(); + ks.set("s".into(), Bytes::from("val"), None, false, false); + assert!(ks.zrandmember("s", None, false).is_err()); + } } diff --git a/crates/ember-core/src/shard/mod.rs b/crates/ember-core/src/shard/mod.rs index 7dd1715c..3eca665f 100644 --- a/crates/ember-core/src/shard/mod.rs +++ b/crates/ember-core/src/shard/mod.rs @@ -455,6 +455,12 @@ pub enum ShardRequest { key: String, fields: Vec, }, + /// HRANDFIELD — returns random field(s) from a hash; read-only, no AOF. + HRandField { + key: String, + count: Option, + with_values: bool, + }, SAdd { key: String, members: Vec, @@ -564,6 +570,12 @@ pub enum ShardRequest { ZUnion { keys: Vec, }, + /// ZRANDMEMBER — returns random member(s) from a sorted set; read-only, no AOF. + ZRandMember { + key: String, + count: Option, + with_scores: bool, + }, /// Returns the key count for this shard. DbSize, /// Returns keyspace stats for this shard. @@ -875,6 +887,10 @@ pub enum ShardResponse { CollectionScan { cursor: u64, items: Vec }, /// HGETALL result: all field-value pairs. HashFields(Vec<(String, Bytes)>), + /// HRANDFIELD result: field names with optional values. + HRandFieldResult(Vec<(String, Option)>), + /// ZRANDMEMBER result: member names with optional scores. + ZRandMemberResult(Vec<(String, Option)>), /// HDEL result: removed count + field names for AOF. HDelLen { count: usize, removed: Vec }, /// Array of strings (e.g. HKEYS). @@ -2017,6 +2033,14 @@ fn dispatch( Ok(vals) => ShardResponse::OptionalArray(vals), Err(_) => ShardResponse::WrongType, }, + ShardRequest::HRandField { + key, + count, + with_values, + } => match ks.hrandfield(key, *count, *with_values) { + Ok(pairs) => ShardResponse::HRandFieldResult(pairs), + Err(_) => ShardResponse::WrongType, + }, ShardRequest::SAdd { key, members } => write_result_len(ks.sadd(key, members)), ShardRequest::SRem { key, members } => match ks.srem(key, members) { Ok(count) => ShardResponse::Len(count), @@ -2122,6 +2146,14 @@ fn dispatch( Ok(pairs) => ShardResponse::ScoredArray(pairs), Err(_) => ShardResponse::WrongType, }, + ShardRequest::ZRandMember { + key, + count, + with_scores, + } => match ks.zrandmember(key, *count, *with_scores) { + Ok(pairs) => ShardResponse::ZRandMemberResult(pairs), + Err(_) => ShardResponse::WrongType, + }, ShardRequest::SScan { key, cursor, diff --git a/crates/ember-protocol/src/command/attributes.rs b/crates/ember-protocol/src/command/attributes.rs index 20f723ca..0079b184 100644 --- a/crates/ember-protocol/src/command/attributes.rs +++ b/crates/ember-protocol/src/command/attributes.rs @@ -133,6 +133,7 @@ impl Command { Command::ZDiff { .. } => "zdiff", Command::ZInter { .. } => "zinter", Command::ZUnion { .. } => "zunion", + Command::ZRandMember { .. } => "zrandmember", // hash Command::HSet { .. } => "hset", @@ -145,6 +146,7 @@ impl Command { Command::HKeys { .. } => "hkeys", Command::HVals { .. } => "hvals", Command::HMGet { .. } => "hmget", + Command::HRandField { .. } => "hrandfield", // set Command::SAdd { .. } => "sadd", @@ -459,9 +461,10 @@ impl Command { Command::Lmpop { .. } => WRITE | LIST | SLOW, // sorted set — reads (Redis 6.2+) - Command::ZDiff { .. } | Command::ZInter { .. } | Command::ZUnion { .. } => { - READ | SORTEDSET | SLOW - } + Command::ZDiff { .. } + | Command::ZInter { .. } + | Command::ZUnion { .. } + | Command::ZRandMember { .. } => READ | SORTEDSET | SLOW, // string extras (Redis 6.2+) Command::GetDel { .. } | Command::GetEx { .. } => WRITE | STRING | FAST, @@ -476,7 +479,8 @@ impl Command { Command::HGetAll { .. } | Command::HKeys { .. } | Command::HVals { .. } - | Command::HMGet { .. } => READ | HASH | SLOW, + | Command::HMGet { .. } + | Command::HRandField { .. } => READ | HASH | SLOW, // hash — writes Command::HSet { .. } | Command::HDel { .. } | Command::HIncrBy { .. } => { @@ -642,6 +646,8 @@ impl Command { | Command::HKeys { key } | Command::HVals { key } | Command::HMGet { key, .. } + | Command::HRandField { key, .. } + | Command::ZRandMember { key, .. } | Command::SAdd { key, .. } | Command::SRem { key, .. } | Command::SMembers { key } diff --git a/crates/ember-protocol/src/command/mod.rs b/crates/ember-protocol/src/command/mod.rs index 1dfec04e..f45ed0c3 100644 --- a/crates/ember-protocol/src/command/mod.rs +++ b/crates/ember-protocol/src/command/mod.rs @@ -476,6 +476,15 @@ pub enum Command { /// HMGET `key` `field` \[field ...\]. Gets multiple field values from a hash. HMGet { key: String, fields: Vec }, + /// HRANDFIELD `key` \[count \[WITHVALUES\]\]. + /// Returns random field(s) from a hash. Positive count returns distinct fields; + /// negative allows duplicates. With WITHVALUES, returns interleaved field-value pairs. + HRandField { + key: String, + count: Option, + with_values: bool, + }, + /// SADD `key` `member` \[member ...\]. Adds members to a set. SAdd { key: String, members: Vec }, @@ -512,6 +521,15 @@ pub enum Command { /// SRANDMEMBER `key` \[count\]. Returns random members without removing them. SRandMember { key: String, count: Option }, + /// ZRANDMEMBER `key` \[count \[WITHSCORES\]\]. + /// Returns random member(s) from a sorted set. Positive count returns distinct members; + /// negative allows duplicates. With WITHSCORES, returns interleaved member-score pairs. + ZRandMember { + key: String, + count: Option, + with_scores: bool, + }, + /// SPOP `key` \[count\]. Removes and returns random members. SPop { key: String, count: usize }, diff --git a/crates/ember-protocol/src/command/parse.rs b/crates/ember-protocol/src/command/parse.rs index 73fb91a4..1fc51656 100644 --- a/crates/ember-protocol/src/command/parse.rs +++ b/crates/ember-protocol/src/command/parse.rs @@ -162,6 +162,7 @@ impl Command { "ZDIFF" => parse_zset_multi("ZDIFF", &frames[1..]), "ZINTER" => parse_zset_multi("ZINTER", &frames[1..]), "ZUNION" => parse_zset_multi("ZUNION", &frames[1..]), + "ZRANDMEMBER" => parse_zrandmember(&frames[1..]), "HSET" => parse_hset(&frames[1..]), "HGET" => parse_hget(&frames[1..]), "HGETALL" => parse_hgetall(&frames[1..]), @@ -172,6 +173,7 @@ impl Command { "HKEYS" => parse_hkeys(&frames[1..]), "HVALS" => parse_hvals(&frames[1..]), "HMGET" => parse_hmget(&frames[1..]), + "HRANDFIELD" => parse_hrandfield(&frames[1..]), "SADD" => parse_sadd(&frames[1..]), "SREM" => parse_srem(&frames[1..]), "SMEMBERS" => parse_smembers(&frames[1..]), @@ -2005,6 +2007,33 @@ fn parse_hmget(args: &[Frame]) -> Result { Ok(Command::HMGet { key, fields }) } +fn parse_hrandfield(args: &[Frame]) -> Result { + if args.is_empty() { + return Err(wrong_arity("HRANDFIELD")); + } + let key = extract_string(&args[0])?; + let (count, with_values) = match args.len() { + 1 => (None, false), + 2 => (Some(parse_i64(&args[1], "HRANDFIELD")?), false), + 3 => { + let count = parse_i64(&args[1], "HRANDFIELD")?; + let flag = extract_string(&args[2])?.to_ascii_uppercase(); + if flag != "WITHVALUES" { + return Err(ProtocolError::InvalidCommandFrame( + "HRANDFIELD: expected WITHVALUES".into(), + )); + } + (Some(count), true) + } + _ => return Err(wrong_arity("HRANDFIELD")), + }; + Ok(Command::HRandField { + key, + count, + with_values, + }) +} + // --- set commands --- fn parse_sadd(args: &[Frame]) -> Result { @@ -3557,6 +3586,33 @@ fn parse_zset_multi(cmd: &'static str, args: &[Frame]) -> Result Result { + if args.is_empty() { + return Err(wrong_arity("ZRANDMEMBER")); + } + let key = extract_string(&args[0])?; + let (count, with_scores) = match args.len() { + 1 => (None, false), + 2 => (Some(parse_i64(&args[1], "ZRANDMEMBER")?), false), + 3 => { + let count = parse_i64(&args[1], "ZRANDMEMBER")?; + let flag = extract_string(&args[2])?.to_ascii_uppercase(); + if flag != "WITHSCORES" { + return Err(ProtocolError::InvalidCommandFrame( + "ZRANDMEMBER: expected WITHSCORES".into(), + )); + } + (Some(count), true) + } + _ => return Err(wrong_arity("ZRANDMEMBER")), + }; + Ok(Command::ZRandMember { + key, + count, + with_scores, + }) +} + fn parse_wait(args: &[Frame]) -> Result { if args.len() != 2 { return Err(wrong_arity("WAIT")); diff --git a/crates/ember-server/src/connection/execute.rs b/crates/ember-server/src/connection/execute.rs index 3578fae2..97ea70c3 100644 --- a/crates/ember-server/src/connection/execute.rs +++ b/crates/ember-server/src/connection/execute.rs @@ -1765,6 +1765,46 @@ pub(super) async fn execute( } } + Command::HRandField { + key, + count, + with_values, + } => { + let idx = engine.shard_for_key(&key); + let req = ShardRequest::HRandField { + key, + count, + with_values, + }; + match engine.send_to_shard(idx, req).await { + Ok(ShardResponse::HRandFieldResult(pairs)) => { + if count.is_none() { + // no count: return a single bulk string (or nil if empty) + match pairs.into_iter().next() { + Some((field, _)) => Frame::Bulk(Bytes::from(field)), + None => Frame::Null, + } + } else { + // with count: return array, interleaved with values if requested + let frames: Vec = pairs + .into_iter() + .flat_map(|(f, v)| { + let mut items = vec![Frame::Bulk(Bytes::from(f))]; + if let Some(val) = v { + items.push(Frame::Bulk(val)); + } + items + }) + .collect(); + Frame::Array(frames) + } + } + Ok(ShardResponse::WrongType) => wrongtype_error(), + Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + Err(e) => Frame::Error(format!("ERR {e}")), + } + } + // --- set commands --- Command::SAdd { key, members } => { let idx = engine.shard_for_key(&key); @@ -2215,6 +2255,46 @@ pub(super) async fn execute( } } + Command::ZRandMember { + key, + count, + with_scores, + } => { + let idx = engine.shard_for_key(&key); + let req = ShardRequest::ZRandMember { + key, + count, + with_scores, + }; + match engine.send_to_shard(idx, req).await { + Ok(ShardResponse::ZRandMemberResult(pairs)) => { + if count.is_none() { + // no count: return a single bulk string (or nil if empty) + match pairs.into_iter().next() { + Some((member, _)) => Frame::Bulk(Bytes::from(member)), + None => Frame::Null, + } + } else { + // with count: return array, interleaved with scores if requested + let frames: Vec = pairs + .into_iter() + .flat_map(|(m, s)| { + let mut items = vec![Frame::Bulk(Bytes::from(m))]; + if let Some(score) = s { + items.push(Frame::Bulk(Bytes::from(score.to_string()))); + } + items + }) + .collect(); + Frame::Array(frames) + } + } + Ok(ShardResponse::WrongType) => wrongtype_error(), + Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + Err(e) => Frame::Error(format!("ERR {e}")), + } + } + Command::SScan { key, cursor, diff --git a/tests/integration/src/data_types.rs b/tests/integration/src/data_types.rs index 239be965..20c1c723 100644 --- a/tests/integration/src/data_types.rs +++ b/tests/integration/src/data_types.rs @@ -599,3 +599,193 @@ async fn lpop_count_exceeding_list_returns_all() { other => panic!("expected Array, got {other:?}"), } } + +// --- HRANDFIELD --- + +#[tokio::test] +async fn hrandfield_no_count_returns_single_field() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["HSET", "h", "a", "1", "b", "2", "c", "3"]).await; + + let resp = c.cmd(&["HRANDFIELD", "h"]).await; + match resp { + Frame::Bulk(b) => { + let field = std::str::from_utf8(&b).unwrap(); + assert!(["a", "b", "c"].contains(&field)); + } + other => panic!("expected Bulk, got {other:?}"), + } +} + +#[tokio::test] +async fn hrandfield_positive_count_distinct() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["HSET", "h", "a", "1", "b", "2", "c", "3"]).await; + + let resp = c.cmd(&["HRANDFIELD", "h", "2"]).await; + match resp { + Frame::Array(frames) => { + assert_eq!(frames.len(), 2); + // all should be valid fields + for f in &frames { + if let Frame::Bulk(b) = f { + let field = std::str::from_utf8(b).unwrap(); + assert!(["a", "b", "c"].contains(&field)); + } else { + panic!("expected Bulk in array, got {f:?}"); + } + } + // distinct + let fields: std::collections::HashSet<_> = frames + .iter() + .map(|f| match f { + Frame::Bulk(b) => std::str::from_utf8(b).unwrap().to_owned(), + _ => unreachable!(), + }) + .collect(); + assert_eq!(fields.len(), 2); + } + other => panic!("expected Array, got {other:?}"), + } +} + +#[tokio::test] +async fn hrandfield_negative_count_allows_duplicates() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["HSET", "h", "only", "val"]).await; + + let resp = c.cmd(&["HRANDFIELD", "h", "-5"]).await; + match resp { + Frame::Array(frames) => { + assert_eq!(frames.len(), 5); + for f in &frames { + if let Frame::Bulk(b) = f { + assert_eq!(std::str::from_utf8(b).unwrap(), "only"); + } else { + panic!("expected Bulk, got {f:?}"); + } + } + } + other => panic!("expected Array, got {other:?}"), + } +} + +#[tokio::test] +async fn hrandfield_withvalues() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["HSET", "h", "field", "value"]).await; + + let resp = c.cmd(&["HRANDFIELD", "h", "1", "WITHVALUES"]).await; + match resp { + Frame::Array(frames) => { + // interleaved: [field, value] + assert_eq!(frames.len(), 2); + assert!(matches!(&frames[0], Frame::Bulk(b) if b == &b"field"[..])); + assert!(matches!(&frames[1], Frame::Bulk(b) if b == &b"value"[..])); + } + other => panic!("expected Array, got {other:?}"), + } +} + +#[tokio::test] +async fn hrandfield_missing_key_empty() { + let server = TestServer::start(); + let mut c = server.connect().await; + + let resp = c.cmd(&["HRANDFIELD", "nosuchkey"]).await; + assert!(matches!(resp, Frame::Null)); + + let resp2 = c.cmd(&["HRANDFIELD", "nosuchkey", "3"]).await; + match resp2 { + Frame::Array(frames) => assert!(frames.is_empty()), + other => panic!("expected empty Array, got {other:?}"), + } +} + +// --- ZRANDMEMBER --- + +#[tokio::test] +async fn zrandmember_no_count_returns_single_member() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["ZADD", "z", "1", "a", "2", "b", "3", "c"]).await; + + let resp = c.cmd(&["ZRANDMEMBER", "z"]).await; + match resp { + Frame::Bulk(b) => { + let member = std::str::from_utf8(&b).unwrap(); + assert!(["a", "b", "c"].contains(&member)); + } + other => panic!("expected Bulk, got {other:?}"), + } +} + +#[tokio::test] +async fn zrandmember_positive_count_distinct() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["ZADD", "z", "1", "a", "2", "b", "3", "c"]).await; + + let resp = c.cmd(&["ZRANDMEMBER", "z", "2"]).await; + match resp { + Frame::Array(frames) => { + assert_eq!(frames.len(), 2); + let members: std::collections::HashSet<_> = frames + .iter() + .map(|f| match f { + Frame::Bulk(b) => std::str::from_utf8(b).unwrap().to_owned(), + _ => panic!("expected Bulk"), + }) + .collect(); + assert_eq!(members.len(), 2); + assert!(members + .iter() + .all(|m| ["a", "b", "c"].contains(&m.as_str()))); + } + other => panic!("expected Array, got {other:?}"), + } +} + +#[tokio::test] +async fn zrandmember_withscores() { + let server = TestServer::start(); + let mut c = server.connect().await; + + c.cmd(&["ZADD", "z", "42", "member"]).await; + + let resp = c.cmd(&["ZRANDMEMBER", "z", "1", "WITHSCORES"]).await; + match resp { + Frame::Array(frames) => { + // interleaved: [member, score] + assert_eq!(frames.len(), 2); + assert!(matches!(&frames[0], Frame::Bulk(b) if b == &b"member"[..])); + assert!(matches!(&frames[1], Frame::Bulk(b) if b == &b"42"[..])); + } + other => panic!("expected Array, got {other:?}"), + } +} + +#[tokio::test] +async fn zrandmember_missing_key_empty() { + let server = TestServer::start(); + let mut c = server.connect().await; + + let resp = c.cmd(&["ZRANDMEMBER", "nosuchkey"]).await; + assert!(matches!(resp, Frame::Null)); + + let resp2 = c.cmd(&["ZRANDMEMBER", "nosuchkey", "3"]).await; + match resp2 { + Frame::Array(frames) => assert!(frames.is_empty()), + other => panic!("expected empty Array, got {other:?}"), + } +}