This commit is contained in:
2025-08-18 12:30:20 +02:00
parent 51ab90c4ad
commit 9177fa4091
5 changed files with 176 additions and 81 deletions

9
herodb/build.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
export SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "I am in $SCRIPT_DIR"
cd "$SCRIPT_DIR"
cargo build

View File

@@ -12,7 +12,6 @@ echo ""
echo "2⃣ Running Comprehensive Redis Integration Tests (13 tests)..." echo "2⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
echo "----------------------------------------------------------------" echo "----------------------------------------------------------------"
cargo test -p herodb --test redis_integration_tests -- --nocapture cargo test -p herodb --test redis_integration_tests -- --nocapture
cargo test -p herodb --test redis_basic_client -- --nocapture
cargo test -p herodb --test debug_hset -- --nocapture cargo test -p herodb --test debug_hset -- --nocapture
cargo test -p herodb --test debug_hset_simple -- --nocapture cargo test -p herodb --test debug_hset_simple -- --nocapture

View File

@@ -12,7 +12,14 @@ impl Storage {
let mut types_table = write_txn.open_table(TYPES_TABLE)?; let mut types_table = write_txn.open_table(TYPES_TABLE)?;
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?; let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
// Set the type to hash let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") | None => { // Proceed if hash or new key
// Set the type to hash (only if new key or existing hash)
types_table.insert(key, "hash")?; types_table.insert(key, "hash")?;
for (field, value) in pairs { for (field, value) in pairs {
@@ -28,6 +35,9 @@ impl Storage {
} }
} }
} }
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
}
}
write_txn.commit()?; write_txn.commit()?;
Ok(new_fields) Ok(new_fields)
@@ -38,8 +48,10 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let key_type = types_table.get(key)?.map(|v| v.value().to_string());
Some(type_val) if type_val.value() == "hash" => {
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
match hashes_table.get((key, field))? { match hashes_table.get((key, field))? {
Some(data) => { Some(data) => {
@@ -50,7 +62,8 @@ impl Storage {
None => Ok(None), None => Ok(None),
} }
} }
_ => Ok(None), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(None),
} }
} }
@@ -58,9 +71,13 @@ impl Storage {
pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> { pub fn hgetall(&self, key: &str) -> Result<Vec<(String, String)>, DBError> {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match types_table.get(key)? { match key_type.as_deref() {
Some(type_val) if type_val.value() == "hash" => { Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new(); let mut result = Vec::new();
@@ -77,7 +94,8 @@ impl Storage {
Ok(result) Ok(result)
} }
_ => Ok(Vec::new()), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(Vec::new()),
} }
} }
@@ -86,16 +104,14 @@ impl Storage {
let mut deleted = 0i64; let mut deleted = 0i64;
// First check if key exists and is a hash // First check if key exists and is a hash
let is_hash = { let key_type = {
let types_table = write_txn.open_table(TYPES_TABLE)?; let types_table = write_txn.open_table(TYPES_TABLE)?;
let result = match types_table.get(key)? { let access_guard = types_table.get(key)?;
Some(type_val) => type_val.value() == "hash", access_guard.map(|v| v.value().to_string())
None => false,
};
result
}; };
if is_hash { match key_type.as_deref() {
Some("hash") => {
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?; let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
for field in fields { for field in fields {
@@ -122,6 +138,9 @@ impl Storage {
types_table.remove(key)?; types_table.remove(key)?;
} }
} }
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => {} // Key does not exist, nothing to delete, return 0 deleted
}
write_txn.commit()?; write_txn.commit()?;
Ok(deleted) Ok(deleted)
@@ -131,12 +150,19 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
Ok(hashes_table.get((key, field))?.is_some()) Ok(hashes_table.get((key, field))?.is_some())
} }
_ => Ok(false), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(false),
} }
} }
@@ -144,8 +170,14 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new(); let mut result = Vec::new();
@@ -160,7 +192,8 @@ impl Storage {
Ok(result) Ok(result)
} }
_ => Ok(Vec::new()), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(Vec::new()),
} }
} }
@@ -169,8 +202,14 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new(); let mut result = Vec::new();
@@ -187,7 +226,8 @@ impl Storage {
Ok(result) Ok(result)
} }
_ => Ok(Vec::new()), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(Vec::new()),
} }
} }
@@ -195,8 +235,14 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut count = 0i64; let mut count = 0i64;
@@ -211,7 +257,8 @@ impl Storage {
Ok(count) Ok(count)
} }
_ => Ok(0), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(0),
} }
} }
@@ -220,8 +267,14 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new(); let mut result = Vec::new();
@@ -238,7 +291,8 @@ impl Storage {
Ok(result) Ok(result)
} }
_ => Ok(fields.into_iter().map(|_| None).collect()), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok(fields.into_iter().map(|_| None).collect()),
} }
} }
@@ -251,9 +305,16 @@ impl Storage {
let mut types_table = write_txn.open_table(TYPES_TABLE)?; let mut types_table = write_txn.open_table(TYPES_TABLE)?;
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?; let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") | None => { // Proceed if hash or new key
// Check if field already exists // Check if field already exists
if hashes_table.get((key, field))?.is_none() { if hashes_table.get((key, field))?.is_none() {
// Set the type to hash // Set the type to hash (only if new key or existing hash)
types_table.insert(key, "hash")?; types_table.insert(key, "hash")?;
// Encrypt the value before storing // Encrypt the value before storing
@@ -262,6 +323,9 @@ impl Storage {
result = true; result = true;
} }
} }
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
}
}
write_txn.commit()?; write_txn.commit()?;
Ok(result) Ok(result)
@@ -272,8 +336,14 @@ impl Storage {
let read_txn = self.db.begin_read()?; let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?; let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? { let types_table = read_txn.open_table(TYPES_TABLE)?;
Some(type_val) if type_val.value() == "hash" => { let key_type = {
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?; let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new(); let mut result = Vec::new();
let mut current_cursor = 0u64; let mut current_cursor = 0u64;
@@ -312,7 +382,8 @@ impl Storage {
let next_cursor = if result.len() < limit { 0 } else { current_cursor }; let next_cursor = if result.len() < limit { 0 } else { current_cursor };
Ok((next_cursor, result)) Ok((next_cursor, result))
} }
_ => Ok((0, Vec::new())), Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
None => Ok((0, Vec::new())),
} }
} }
} }

View File

@@ -25,7 +25,7 @@ impl Storage {
}; };
// Add elements to the front (left) // Add elements to the front (left)
for element in elements.into_iter().rev() { for element in elements.into_iter() {
list.insert(0, element); list.insert(0, element);
} }

View File

@@ -93,9 +93,16 @@ async fn test_basic_redis_functionality() {
assert!(response.contains("string")); assert!(response.contains("string"));
// Test QUIT to close connection gracefully // Test QUIT to close connection gracefully
let response = send_redis_command(port, "*1\r\n$4\r\nQUIT\r\n").await; let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await.unwrap();
stream.write_all("*1\r\n$4\r\nQUIT\r\n".as_bytes()).await.unwrap();
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer).await.unwrap();
let response = String::from_utf8_lossy(&buffer[..n]);
assert!(response.contains("OK")); assert!(response.contains("OK"));
// Ensure the stream is closed
stream.shutdown().await.unwrap();
// Stop the server // Stop the server
server_handle.abort(); server_handle.abort();
@@ -149,6 +156,8 @@ async fn test_hash_operations() {
assert!(response.contains("value2")); assert!(response.contains("value2"));
// Stop the server // Stop the server
// For hash operations, we don't have a persistent stream, so we'll just abort the server.
// The server should handle closing its connections.
server_handle.abort(); server_handle.abort();
println!("✅ All hash operations tests passed!"); println!("✅ All hash operations tests passed!");
@@ -202,9 +211,16 @@ async fn test_transaction_operations() {
assert!(response.contains("OK")); // Should contain array of OK responses assert!(response.contains("OK")); // Should contain array of OK responses
// Verify commands were executed // Verify commands were executed
let response = send_redis_command(port, "*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n").await; stream.write_all("*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n".as_bytes()).await.unwrap();
let n = stream.read(&mut buffer).await.unwrap();
let response = String::from_utf8_lossy(&buffer[..n]);
assert!(response.contains("value1")); assert!(response.contains("value1"));
stream.write_all("*2\r\n$3\r\nGET\r\n$4\r\nkey2\r\n".as_bytes()).await.unwrap();
let n = stream.read(&mut buffer).await.unwrap();
let response = String::from_utf8_lossy(&buffer[..n]);
assert!(response.contains("value2"));
// Stop the server // Stop the server
server_handle.abort(); server_handle.abort();