...
This commit is contained in:
		
							
								
								
									
										9
									
								
								herodb/build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								herodb/build.sh
									
									
									
									
									
										Executable 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 | ||||
|  | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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())), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|             } | ||||
|              | ||||
|   | ||||
| @@ -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(); | ||||
|      | ||||
|   | ||||
		Reference in New Issue
	
	Block a user