This commit is contained in:
despiegk 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 "----------------------------------------------------------------"
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_simple -- --nocapture

View File

@ -12,20 +12,30 @@ impl Storage {
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
// Set the type to hash
types_table.insert(key, "hash")?;
for (field, value) in pairs {
// Check if field already exists
let exists = hashes_table.get((key, field.as_str()))?.is_some();
// Encrypt the value before storing
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
hashes_table.insert((key, field.as_str()), encrypted.as_slice())?;
if !exists {
new_fields += 1;
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")?;
for (field, value) in pairs {
// Check if field already exists
let exists = hashes_table.get((key, field.as_str()))?.is_some();
// Encrypt the value before storing
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
hashes_table.insert((key, field.as_str()), encrypted.as_slice())?;
if !exists {
new_fields += 1;
}
}
}
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
}
}
@ -38,8 +48,10 @@ impl Storage {
let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
let key_type = types_table.get(key)?.map(|v| v.value().to_string());
match key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
match hashes_table.get((key, field))? {
Some(data) => {
@ -50,7 +62,8 @@ impl Storage {
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> {
let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
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 mut result = Vec::new();
@ -77,7 +94,8 @@ impl Storage {
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,41 +104,42 @@ impl Storage {
let mut deleted = 0i64;
// 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 result = match types_table.get(key)? {
Some(type_val) => type_val.value() == "hash",
None => false,
};
result
let access_guard = types_table.get(key)?;
access_guard.map(|v| v.value().to_string())
};
if is_hash {
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
for field in fields {
if hashes_table.remove((key, field.as_str()))?.is_some() {
deleted += 1;
match key_type.as_deref() {
Some("hash") => {
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
for field in fields {
if hashes_table.remove((key, field.as_str()))?.is_some() {
deleted += 1;
}
}
// Check if hash is now empty and remove type if so
let mut has_fields = false;
let mut iter = hashes_table.iter()?;
while let Some(entry) = iter.next() {
let entry = entry?;
let (hash_key, _) = entry.0.value();
if hash_key == key {
has_fields = true;
break;
}
}
drop(iter);
if !has_fields {
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
types_table.remove(key)?;
}
}
// Check if hash is now empty and remove type if so
let mut has_fields = false;
let mut iter = hashes_table.iter()?;
while let Some(entry) = iter.next() {
let entry = entry?;
let (hash_key, _) = entry.0.value();
if hash_key == key {
has_fields = true;
break;
}
}
drop(iter);
if !has_fields {
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
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()?;
@ -131,12 +150,19 @@ impl Storage {
let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
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 types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new();
@ -160,7 +192,8 @@ impl Storage {
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 types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new();
@ -187,7 +226,8 @@ impl Storage {
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 types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut count = 0i64;
@ -211,7 +257,8 @@ impl Storage {
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 types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new();
@ -238,7 +291,8 @@ impl Storage {
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,15 +305,25 @@ impl Storage {
let mut types_table = write_txn.open_table(TYPES_TABLE)?;
let mut hashes_table = write_txn.open_table(HASHES_TABLE)?;
// Check if field already exists
if hashes_table.get((key, field))?.is_none() {
// Set the type to hash
types_table.insert(key, "hash")?;
// Encrypt the value before storing
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
hashes_table.insert((key, field), encrypted.as_slice())?;
result = true;
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
if hashes_table.get((key, field))?.is_none() {
// Set the type to hash (only if new key or existing hash)
types_table.insert(key, "hash")?;
// Encrypt the value before storing
let encrypted = self.encrypt_if_needed(value.as_bytes())?;
hashes_table.insert((key, field), encrypted.as_slice())?;
result = true;
}
}
Some(_) => return Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
}
}
@ -272,8 +336,14 @@ impl Storage {
let read_txn = self.db.begin_read()?;
let types_table = read_txn.open_table(TYPES_TABLE)?;
match types_table.get(key)? {
Some(type_val) if type_val.value() == "hash" => {
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 key_type.as_deref() {
Some("hash") => {
let hashes_table = read_txn.open_table(HASHES_TABLE)?;
let mut result = Vec::new();
let mut current_cursor = 0u64;
@ -312,7 +382,8 @@ impl Storage {
let next_cursor = if result.len() < limit { 0 } else { current_cursor };
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)
for element in elements.into_iter().rev() {
for element in elements.into_iter() {
list.insert(0, element);
}

View File

@ -93,9 +93,16 @@ async fn test_basic_redis_functionality() {
assert!(response.contains("string"));
// 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"));
// Ensure the stream is closed
stream.shutdown().await.unwrap();
// Stop the server
server_handle.abort();
@ -149,6 +156,8 @@ async fn test_hash_operations() {
assert!(response.contains("value2"));
// 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();
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
// 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"));
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
server_handle.abort();