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..89578e8 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.created_at.cmp(&a.created_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..a0ab6b3 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 created_at = removed.map(|v| v.created_at).unwrap_or(Instant::now());
let value = Value {
data,
expires_at: None,
+ created_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 created_at = removed.map(|v| v.created_at).unwrap_or(Instant::now());
let expires_at = Instant::now() + ttl;
let value = Value {
data,
expires_at: Some(expires_at),
+ created_at,
};
self.state.keys.insert(key.clone(), value);
@@ -142,11 +148,8 @@ impl<'a> InnerStoreLocked<'a> {
self.state.keys.keys()
}
- pub fn iter(&self) -> impl Iterator- {
- self.state
- .keys
- .iter()
- .map(|(key, value)| (key, &value.data))
+ pub fn iter(&self) -> impl Iterator
- {
+ self.state.keys.iter().map(|(key, value)| (key, value))
}
pub fn incr_by(&mut self, key: &str, increment: T) -> Result
@@ -232,6 +235,7 @@ type Key = String;
pub struct Value {
pub data: Bytes,
pub expires_at: Option,
+ pub created_at: Instant,
}
pub struct State {
diff --git a/tests/integration.rs b/tests/integration.rs
index fa19c88..5d5fc7e 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -363,15 +363,22 @@ 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 deterministic (always returning the same order for
+ // a given set of keys) but not guaranteed (it may change between runs).
+ //
+ // We sort in backward chronological order to get deterministic results.
+ // 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");
p.cmd("KEYS").arg("*");
p.cmd("KEYS").arg("*key*");
+
+ p.cmd("SET").arg("keys_key_2").arg("Spain");
+ p.cmd("SET").arg("keys_key_3").arg("Netherlands");
+
+ p.cmd("KEYS").arg("*1");
+ p.cmd("KEYS").arg("*2");
p.cmd("KEYS").arg("*3");
})
.await;