diff --git a/Cargo.toml b/Cargo.toml
index 74c1c7c..d86f30b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ strum = "0.26.2"
strum_macros = "0.26.2"
clap = { version = "4.5.7", features = ["derive", "env"] }
num-traits = "0.2.19"
+itertools = "0.13.0"
[dev-dependencies]
rand = "0.8.5"
diff --git a/src/commands/keys.rs b/src/commands/keys.rs
index 203c8fe..d79c975 100644
--- a/src/commands/keys.rs
+++ b/src/commands/keys.rs
@@ -1,5 +1,6 @@
use bytes::Bytes;
use glob_match::glob_match;
+use itertools::Itertools;
use crate::commands::executable::Executable;
use crate::commands::CommandParser;
@@ -20,11 +21,12 @@ pub struct Keys {
impl Executable for Keys {
fn exec(self, store: Store) -> Result {
let store = store.lock();
- let matching_keys: Vec = store
- .keys()
- .filter(|key| glob_match(self.pattern.as_str(), key))
- .map(|key| Frame::Bulk(Bytes::from(key.to_string())))
- .collect();
+ let matching_keys = store
+ .iter()
+ .filter(|(key, _)| glob_match(self.pattern.as_str(), key))
+ .sorted_by(|(_, a), (_, b)| b.inserted_at.cmp(&a.inserted_at))
+ .map(|(key, _)| Frame::Bulk(Bytes::from(key.to_string())))
+ .collect::>();
Ok(Frame::Array(matching_keys))
}
diff --git a/src/store.rs b/src/store.rs
index b83a575..980fd1d 100644
--- a/src/store.rs
+++ b/src/store.rs
@@ -59,23 +59,29 @@ pub struct InnerStoreLocked<'a> {
impl<'a> InnerStoreLocked<'a> {
pub fn set(&mut self, key: String, data: Bytes) {
// Ensure any previous TTL is removed.
- self.remove(&key);
+ let removed = self.remove(&key);
+
+ let inserted_at = removed.map(|v| v.inserted_at).unwrap_or(Instant::now());
let value = Value {
data,
expires_at: None,
+ inserted_at,
};
self.state.keys.insert(key, value);
}
pub fn set_with_ttl(&mut self, key: Key, data: Bytes, ttl: Duration) {
// Ensure any previous TTL is removed.
- self.remove(&key);
+ let removed = self.remove(&key);
+
+ let inserted_at = removed.map(|v| v.inserted_at).unwrap_or(Instant::now());
let expires_at = Instant::now() + ttl;
let value = Value {
data,
expires_at: Some(expires_at),
+ inserted_at,
};
self.state.keys.insert(key.clone(), value);
@@ -142,11 +148,11 @@ impl<'a> InnerStoreLocked<'a> {
self.state.keys.keys()
}
- pub fn iter(&self) -> impl Iterator- {
+ pub fn iter(&self) -> impl Iterator
- {
self.state
.keys
.iter()
- .map(|(key, value)| (key, &value.data))
+ .map(|(key, value)| (key, value))
}
pub fn incr_by(&mut self, key: &str, increment: T) -> Result
@@ -232,6 +238,7 @@ type Key = String;
pub struct Value {
pub data: Bytes,
pub expires_at: Option,
+ pub inserted_at: Instant,
}
pub struct State {
diff --git a/tests/integration.rs b/tests/integration.rs
index 0e12570..99434ae 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -349,9 +349,11 @@ async fn test_getrange() {
#[tokio::test]
#[serial]
async fn test_keys() {
- // TODO: The response order from the server is not guaranteed, to ensure accurate comparison
- // with the expected result, we need to sort the response before performing the comparison.
test_compare::>(|p| {
+ // Redis keys order is deterministice but not guaranteed.
+ // We sort the keys by insertion order to make the test deterministic.
+ // Testing with a different set of keys produces different results,
+ // but matching the implementation is out of the scope of the project.
p.cmd("SET").arg("keys_key_1").arg("Argentina");
p.cmd("SET").arg("keys_key_2").arg("Spain");
p.cmd("SET").arg("keys_key_3").arg("Netherlands");