...
This commit is contained in:
		
							
								
								
									
										87
									
								
								src/cmd.rs
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								src/cmd.rs
									
									
									
									
									
								
							| @@ -4,6 +4,7 @@ use crate::{error::DBError, protocol::Protocol, server::Server}; | ||||
| pub enum Cmd { | ||||
|     Ping, | ||||
|     Echo(String), | ||||
|     Select(u16), | ||||
|     Get(String), | ||||
|     Set(String, String), | ||||
|     SetPx(String, String, u128), | ||||
| @@ -60,6 +61,13 @@ impl Cmd { | ||||
|                 } | ||||
|                 Ok(( | ||||
|                     match cmd[0].to_lowercase().as_str() { | ||||
|                         "select" => { | ||||
|                             if cmd.len() != 2 { | ||||
|                                 return Err(DBError("wrong number of arguments for SELECT".to_string())); | ||||
|                             } | ||||
|                             let idx = cmd[1].parse::<u16>().map_err(|_| DBError("ERR DB index is not an integer".to_string()))?; | ||||
|                             Cmd::Select(idx) | ||||
|                         } | ||||
|                         "echo" => Cmd::Echo(cmd[1].clone()), | ||||
|                         "ping" => Cmd::Ping, | ||||
|                         "get" => Cmd::Get(cmd[1].clone()), | ||||
| @@ -419,6 +427,7 @@ impl Cmd { | ||||
|         } | ||||
|  | ||||
|         match self { | ||||
|             Cmd::Select(db) => select_cmd(server, *db).await, | ||||
|             Cmd::Ping => Ok(Protocol::SimpleString("PONG".to_string())), | ||||
|             Cmd::Echo(s) => Ok(Protocol::SimpleString(s.clone())), | ||||
|             Cmd::Get(k) => get_cmd(server, k).await, | ||||
| @@ -480,9 +489,17 @@ impl Cmd { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| async fn select_cmd(server: &mut Server, db: u16) -> Result<Protocol, DBError> { | ||||
|     let idx = db as usize; | ||||
|     if idx >= server.storages.len() { | ||||
|         return Ok(Protocol::err("ERR DB index is out of range")); | ||||
|     } | ||||
|     server.selected_db = idx; | ||||
|     Ok(Protocol::SimpleString("OK".to_string())) | ||||
| } | ||||
|  | ||||
| async fn lindex_cmd(server: &Server, key: &str, index: i64) -> Result<Protocol, DBError> { | ||||
|     match server.storage.lindex(key, index) { | ||||
|     match server.current_storage().lindex(key, index) { | ||||
|         Ok(Some(element)) => Ok(Protocol::BulkString(element)), | ||||
|         Ok(None) => Ok(Protocol::Null), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
| @@ -490,35 +507,35 @@ async fn lindex_cmd(server: &Server, key: &str, index: i64) -> Result<Protocol, | ||||
| } | ||||
|  | ||||
| async fn lrange_cmd(server: &Server, key: &str, start: i64, stop: i64) -> Result<Protocol, DBError> { | ||||
|     match server.storage.lrange(key, start, stop) { | ||||
|     match server.current_storage().lrange(key, start, stop) { | ||||
|         Ok(elements) => Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn ltrim_cmd(server: &Server, key: &str, start: i64, stop: i64) -> Result<Protocol, DBError> { | ||||
|     match server.storage.ltrim(key, start, stop) { | ||||
|     match server.current_storage().ltrim(key, start, stop) { | ||||
|         Ok(_) => Ok(Protocol::SimpleString("OK".to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn lrem_cmd(server: &Server, key: &str, count: i64, element: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.lrem(key, count, element) { | ||||
|     match server.current_storage().lrem(key, count, element) { | ||||
|         Ok(removed_count) => Ok(Protocol::SimpleString(removed_count.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn llen_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.llen(key) { | ||||
|     match server.current_storage().llen(key) { | ||||
|         Ok(len) => Ok(Protocol::SimpleString(len.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn lpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> { | ||||
|     match server.storage.lpop(key, *count) { | ||||
|     match server.current_storage().lpop(key, *count) { | ||||
|         Ok(Some(elements)) => { | ||||
|             if count.is_some() { | ||||
|                 Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect())) | ||||
| @@ -538,7 +555,7 @@ async fn lpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Pro | ||||
| } | ||||
|  | ||||
| async fn rpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Protocol, DBError> { | ||||
|     match server.storage.rpop(key, *count) { | ||||
|     match server.current_storage().rpop(key, *count) { | ||||
|         Ok(Some(elements)) => { | ||||
|             if count.is_some() { | ||||
|                 Ok(Protocol::Array(elements.into_iter().map(Protocol::BulkString).collect())) | ||||
| @@ -558,14 +575,14 @@ async fn rpop_cmd(server: &Server, key: &str, count: &Option<u64>) -> Result<Pro | ||||
| } | ||||
|  | ||||
| async fn lpush_cmd(server: &Server, key: &str, elements: &[String]) -> Result<Protocol, DBError> { | ||||
|     match server.storage.lpush(key, elements.to_vec()) { | ||||
|     match server.current_storage().lpush(key, elements.to_vec()) { | ||||
|         Ok(len) => Ok(Protocol::SimpleString(len.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn rpush_cmd(server: &Server, key: &str, elements: &[String]) -> Result<Protocol, DBError> { | ||||
|     match server.storage.rpush(key, elements.to_vec()) { | ||||
|     match server.current_storage().rpush(key, elements.to_vec()) { | ||||
|         Ok(len) => Ok(Protocol::SimpleString(len.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| @@ -589,7 +606,7 @@ async fn exec_cmd( | ||||
| } | ||||
|  | ||||
| async fn incr_cmd(server: &Server, key: &String) -> Result<Protocol, DBError> { | ||||
|     let current_value = server.storage.get(key)?; | ||||
|     let current_value = server.current_storage().get(key)?; | ||||
|      | ||||
|     let new_value = match current_value { | ||||
|         Some(v) => { | ||||
| @@ -601,7 +618,7 @@ async fn incr_cmd(server: &Server, key: &String) -> Result<Protocol, DBError> { | ||||
|         None => 1, | ||||
|     }; | ||||
|      | ||||
|     server.storage.set(key.clone(), new_value.to_string())?; | ||||
|     server.current_storage().set(key.clone(), new_value.to_string())?; | ||||
|     Ok(Protocol::SimpleString(new_value.to_string())) | ||||
| } | ||||
|  | ||||
| @@ -613,18 +630,18 @@ fn config_get_cmd(name: &String, server: &Server) -> Result<Protocol, DBError> { | ||||
|         ])), | ||||
|         "dbfilename" => Ok(Protocol::Array(vec![ | ||||
|             Protocol::BulkString(name.clone()), | ||||
|             Protocol::BulkString("herodb.redb".to_string()), | ||||
|             Protocol::BulkString(format!("{}.db", server.selected_db)), | ||||
|         ])), | ||||
|         "databases" => Ok(Protocol::Array(vec![ | ||||
|             Protocol::BulkString(name.clone()), | ||||
|             Protocol::BulkString("16".to_string()), | ||||
|             Protocol::BulkString(server.option.databases.to_string()), | ||||
|         ])), | ||||
|         _ => Ok(Protocol::Array(vec![])), // Return empty array for unknown configs instead of error | ||||
|         _ => Ok(Protocol::Array(vec![])), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn keys_cmd(server: &Server) -> Result<Protocol, DBError> { | ||||
|     let keys = server.storage.keys("*")?; | ||||
|     let keys = server.current_storage().keys("*")?; | ||||
|     Ok(Protocol::Array( | ||||
|         keys.into_iter().map(Protocol::BulkString).collect(), | ||||
|     )) | ||||
| @@ -643,14 +660,14 @@ fn info_cmd(section: &Option<String>) -> Result<Protocol, DBError> { | ||||
| } | ||||
|  | ||||
| async fn type_cmd(server: &Server, k: &String) -> Result<Protocol, DBError> { | ||||
|     match server.storage.get_key_type(k)? { | ||||
|     match server.current_storage().get_key_type(k)? { | ||||
|         Some(type_str) => Ok(Protocol::SimpleString(type_str)), | ||||
|         None => Ok(Protocol::SimpleString("none".to_string())), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn del_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> { | ||||
|     server.storage.del(k.to_string())?; | ||||
|     server.current_storage().del(k.to_string())?; | ||||
|     Ok(Protocol::SimpleString("1".to_string())) | ||||
| } | ||||
|  | ||||
| @@ -660,7 +677,7 @@ async fn set_ex_cmd( | ||||
|     v: &str, | ||||
|     x: &u128, | ||||
| ) -> Result<Protocol, DBError> { | ||||
|     server.storage.setx(k.to_string(), v.to_string(), *x * 1000)?; | ||||
|     server.current_storage().setx(k.to_string(), v.to_string(), *x * 1000)?; | ||||
|     Ok(Protocol::SimpleString("OK".to_string())) | ||||
| } | ||||
|  | ||||
| @@ -670,28 +687,28 @@ async fn set_px_cmd( | ||||
|     v: &str, | ||||
|     x: &u128, | ||||
| ) -> Result<Protocol, DBError> { | ||||
|     server.storage.setx(k.to_string(), v.to_string(), *x)?; | ||||
|     server.current_storage().setx(k.to_string(), v.to_string(), *x)?; | ||||
|     Ok(Protocol::SimpleString("OK".to_string())) | ||||
| } | ||||
|  | ||||
| async fn set_cmd(server: &Server, k: &str, v: &str) -> Result<Protocol, DBError> { | ||||
|     server.storage.set(k.to_string(), v.to_string())?; | ||||
|     server.current_storage().set(k.to_string(), v.to_string())?; | ||||
|     Ok(Protocol::SimpleString("OK".to_string())) | ||||
| } | ||||
|  | ||||
| async fn get_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> { | ||||
|     let v = server.storage.get(k)?; | ||||
|     let v = server.current_storage().get(k)?; | ||||
|     Ok(v.map_or(Protocol::Null, Protocol::BulkString)) | ||||
| } | ||||
|  | ||||
| // Hash command implementations | ||||
| async fn hset_cmd(server: &Server, key: &str, pairs: &[(String, String)]) -> Result<Protocol, DBError> { | ||||
|     let new_fields = server.storage.hset(key, pairs)?; | ||||
|     let new_fields = server.current_storage().hset(key, pairs)?; | ||||
|     Ok(Protocol::SimpleString(new_fields.to_string())) | ||||
| } | ||||
|  | ||||
| async fn hget_cmd(server: &Server, key: &str, field: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hget(key, field) { | ||||
|     match server.current_storage().hget(key, field) { | ||||
|         Ok(Some(value)) => Ok(Protocol::BulkString(value)), | ||||
|         Ok(None) => Ok(Protocol::Null), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
| @@ -699,7 +716,7 @@ async fn hget_cmd(server: &Server, key: &str, field: &str) -> Result<Protocol, D | ||||
| } | ||||
|  | ||||
| async fn hgetall_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hgetall(key) { | ||||
|     match server.current_storage().hgetall(key) { | ||||
|         Ok(pairs) => { | ||||
|             let mut result = Vec::new(); | ||||
|             for (field, value) in pairs { | ||||
| @@ -713,21 +730,21 @@ async fn hgetall_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
| } | ||||
|  | ||||
| async fn hdel_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hdel(key, fields) { | ||||
|     match server.current_storage().hdel(key, fields) { | ||||
|         Ok(deleted) => Ok(Protocol::SimpleString(deleted.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn hexists_cmd(server: &Server, key: &str, field: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hexists(key, field) { | ||||
|     match server.current_storage().hexists(key, field) { | ||||
|         Ok(exists) => Ok(Protocol::SimpleString(if exists { "1" } else { "0" }.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn hkeys_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hkeys(key) { | ||||
|     match server.current_storage().hkeys(key) { | ||||
|         Ok(keys) => Ok(Protocol::Array( | ||||
|             keys.into_iter().map(Protocol::BulkString).collect(), | ||||
|         )), | ||||
| @@ -736,7 +753,7 @@ async fn hkeys_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
| } | ||||
|  | ||||
| async fn hvals_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hvals(key) { | ||||
|     match server.current_storage().hvals(key) { | ||||
|         Ok(values) => Ok(Protocol::Array( | ||||
|             values.into_iter().map(Protocol::BulkString).collect(), | ||||
|         )), | ||||
| @@ -745,14 +762,14 @@ async fn hvals_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
| } | ||||
|  | ||||
| async fn hlen_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hlen(key) { | ||||
|     match server.current_storage().hlen(key) { | ||||
|         Ok(len) => Ok(Protocol::SimpleString(len.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn hmget_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hmget(key, fields) { | ||||
|     match server.current_storage().hmget(key, fields) { | ||||
|         Ok(values) => { | ||||
|             let result: Vec<Protocol> = values | ||||
|                 .into_iter() | ||||
| @@ -765,14 +782,14 @@ async fn hmget_cmd(server: &Server, key: &str, fields: &[String]) -> Result<Prot | ||||
| } | ||||
|  | ||||
| async fn hsetnx_cmd(server: &Server, key: &str, field: &str, value: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hsetnx(key, field, value) { | ||||
|     match server.current_storage().hsetnx(key, field, value) { | ||||
|         Ok(was_set) => Ok(Protocol::SimpleString(if was_set { "1" } else { "0" }.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn scan_cmd(server: &Server, cursor: &u64, pattern: Option<&str>, count: &Option<u64>) -> Result<Protocol, DBError> { | ||||
|     match server.storage.scan(*cursor, pattern, *count) { | ||||
|     match server.current_storage().scan(*cursor, pattern, *count) { | ||||
|         Ok((next_cursor, keys)) => { | ||||
|             let mut result = Vec::new(); | ||||
|             result.push(Protocol::BulkString(next_cursor.to_string())); | ||||
| @@ -786,7 +803,7 @@ async fn scan_cmd(server: &Server, cursor: &u64, pattern: Option<&str>, count: & | ||||
| } | ||||
|  | ||||
| async fn hscan_cmd(server: &Server, key: &str, cursor: &u64, pattern: Option<&str>, count: &Option<u64>) -> Result<Protocol, DBError> { | ||||
|     match server.storage.hscan(key, *cursor, pattern, *count) { | ||||
|     match server.current_storage().hscan(key, *cursor, pattern, *count) { | ||||
|         Ok((next_cursor, fields)) => { | ||||
|             let mut result = Vec::new(); | ||||
|             result.push(Protocol::BulkString(next_cursor.to_string())); | ||||
| @@ -800,14 +817,14 @@ async fn hscan_cmd(server: &Server, key: &str, cursor: &u64, pattern: Option<&st | ||||
| } | ||||
|  | ||||
| async fn ttl_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.ttl(key) { | ||||
|     match server.current_storage().ttl(key) { | ||||
|         Ok(ttl) => Ok(Protocol::SimpleString(ttl.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn exists_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> { | ||||
|     match server.storage.exists(key) { | ||||
|     match server.current_storage().exists(key) { | ||||
|         Ok(exists) => Ok(Protocol::SimpleString(if exists { "1" } else { "0" }.to_string())), | ||||
|         Err(e) => Ok(Protocol::err(&e.0)), | ||||
|     } | ||||
|   | ||||
| @@ -22,6 +22,10 @@ struct Args { | ||||
|     /// Enable debug mode | ||||
|     #[arg(long)] | ||||
|     debug: bool, | ||||
|  | ||||
|     /// Number of logical databases (SELECT 0..N-1) | ||||
|     #[arg(long, default_value_t = 16)] | ||||
|     databases: u16, | ||||
| } | ||||
|  | ||||
| #[tokio::main] | ||||
| @@ -41,6 +45,7 @@ async fn main() { | ||||
|         dir: args.dir, | ||||
|         port, | ||||
|         debug: args.debug, | ||||
|         databases: args.databases, | ||||
|     }; | ||||
|  | ||||
|     // new server | ||||
|   | ||||
| @@ -3,4 +3,5 @@ pub struct DBOption { | ||||
|     pub dir: String, | ||||
|     pub port: u16, | ||||
|     pub debug: bool, | ||||
|     pub databases: u16, // number of logical DBs (default 16) | ||||
| } | ||||
|   | ||||
| @@ -12,27 +12,36 @@ use crate::storage::Storage; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct Server { | ||||
|     pub storage: Arc<Storage>, | ||||
|     pub storages: Vec<Arc<Storage>>, | ||||
|     pub option: options::DBOption, | ||||
|     pub client_name: Option<String>, | ||||
|     pub selected_db: usize, // per-connection | ||||
| } | ||||
|  | ||||
| impl Server { | ||||
|     pub async fn new(option: options::DBOption) -> Self { | ||||
|         // Create database file path with fixed filename | ||||
|         let db_file_path = PathBuf::from(option.dir.clone()).join("herodb.redb"); | ||||
|         println!("will open db file path: {}", db_file_path.display()); | ||||
|  | ||||
|         // Initialize storage with redb | ||||
|         let storage = Storage::new(db_file_path).expect("Failed to initialize storage"); | ||||
|         // Eagerly create N db files: <dir>/<index>.db | ||||
|         let mut storages = Vec::with_capacity(option.databases as usize); | ||||
|         for i in 0..option.databases { | ||||
|             let db_file_path = PathBuf::from(option.dir.clone()).join(format!("{}.db", i)); | ||||
|             println!("will open db file path (db {}): {}", i, db_file_path.display()); | ||||
|             let storage = Storage::new(db_file_path).expect("Failed to initialize storage"); | ||||
|             storages.push(Arc::new(storage)); | ||||
|         } | ||||
|  | ||||
|         Server { | ||||
|             storage: Arc::new(storage), | ||||
|             storages, | ||||
|             option, | ||||
|             client_name: None, | ||||
|             selected_db: 0, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub fn current_storage(&self) -> &Storage { | ||||
|         self.storages[self.selected_db].as_ref() | ||||
|     } | ||||
|  | ||||
|     pub async fn handle( | ||||
|         &mut self, | ||||
|         mut stream: tokio::net::TcpStream, | ||||
|   | ||||
| @@ -25,6 +25,7 @@ async fn debug_hset_simple() { | ||||
|         dir: test_dir.to_string(), | ||||
|         port, | ||||
|         debug: false, | ||||
|         databases: 16, | ||||
|     }; | ||||
|      | ||||
|     let mut server = Server::new(option).await; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ async fn debug_hset_return_value() { | ||||
|         dir: test_dir.to_string(), | ||||
|         port: 16390, | ||||
|         debug: false, | ||||
|         databases: 16, | ||||
|     }; | ||||
|      | ||||
|     let mut server = Server::new(option).await; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| use redis::{Client, Commands, Connection}; | ||||
| use redis::{Client, Commands, Connection, RedisResult}; | ||||
| use std::process::{Child, Command}; | ||||
| use std::time::Duration; | ||||
| use tokio::time::sleep; | ||||
| @@ -77,6 +77,7 @@ fn setup_server() -> (ServerProcessGuard, u16) { | ||||
|             &test_dir, | ||||
|             "--port", | ||||
|             &port.to_string(), | ||||
|             "--debug", | ||||
|         ]) | ||||
|         .spawn() | ||||
|         .expect("Failed to start server process"); | ||||
| @@ -88,273 +89,174 @@ fn setup_server() -> (ServerProcessGuard, u16) { | ||||
|     }; | ||||
|      | ||||
|     // Give the server a moment to start | ||||
|     std::thread::sleep(Duration::from_millis(100)); | ||||
|     std::thread::sleep(Duration::from_millis(500)); | ||||
|  | ||||
|     (guard, port) | ||||
| } | ||||
|  | ||||
| async fn cleanup_keys(conn: &mut Connection) { | ||||
|     let keys: Vec<String> = redis::cmd("KEYS").arg("*").query(conn).unwrap(); | ||||
|     if !keys.is_empty() { | ||||
|         for key in keys { | ||||
|             let _: () = redis::cmd("DEL").arg(key).query(conn).unwrap(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn all_tests() { | ||||
|     let (_server_guard, port) = setup_server(); | ||||
|     let mut conn = get_redis_connection(port); | ||||
|  | ||||
|     // Run all tests using the same connection | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_basic_ping(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_string_operations(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_incr_operations(&mut conn).await; | ||||
|     // cleanup_keys(&mut conn).await; | ||||
|     // test_hash_operations(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_hash_operations(&mut conn).await; | ||||
|     test_expiration(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_scan_operations(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_scan_with_count(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_hscan_operations(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_transaction_operations(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_discard_transaction(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_type_command(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_config_commands(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_info_command(&mut conn).await; | ||||
|     cleanup_keys(&mut conn).await; | ||||
|     test_error_handling(&mut conn).await; | ||||
|      | ||||
|     // Clean up keys after all tests | ||||
|     cleanup_keys(&mut conn).await; | ||||
| } | ||||
|  | ||||
| async fn cleanup_keys(conn: &mut Connection) { | ||||
|     let keys: Vec<String> = redis::cmd("KEYS").arg("*").query(conn).unwrap(); | ||||
|     if !keys.is_empty() { | ||||
|         let _: () = redis::cmd("DEL").arg(keys).query(conn).unwrap(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn test_basic_ping(conn: &mut Connection) { | ||||
|     cleanup_keys(conn).await; | ||||
|     let result: String = redis::cmd("PING").query(conn).unwrap(); | ||||
|     assert_eq!(result, "PONG"); | ||||
|  | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_string_operations(conn: &mut Connection) { | ||||
|     // Test SET | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = conn.set("key", "value").unwrap(); | ||||
|  | ||||
|     // Test GET | ||||
|     let result: String = conn.get("key").unwrap(); | ||||
|     assert_eq!(result, "value"); | ||||
|  | ||||
|     // Test GET non-existent key | ||||
|     let result: Option<String> = conn.get("noexist").unwrap(); | ||||
|     assert_eq!(result, None); | ||||
|  | ||||
|     // Test DEL | ||||
|     let deleted: i32 = conn.del("key").unwrap(); | ||||
|     assert_eq!(deleted, 1); | ||||
|  | ||||
|     // Test GET after DEL | ||||
|     let result: Option<String> = conn.get("key").unwrap(); | ||||
|     assert_eq!(result, None); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_incr_operations(conn: &mut Connection) { | ||||
|     // Test INCR on non-existent key | ||||
|     cleanup_keys(conn).await; | ||||
|     let result: i32 = redis::cmd("INCR").arg("counter").query(conn).unwrap(); | ||||
|     assert_eq!(result, 1); | ||||
|  | ||||
|     // Test INCR on existing key | ||||
|     let result: i32 = redis::cmd("INCR").arg("counter").query(conn).unwrap(); | ||||
|     assert_eq!(result, 2); | ||||
|  | ||||
|     // Test INCR on string value (should fail) | ||||
|     let _: () = conn.set("string", "hello").unwrap(); | ||||
|     let result: Result<i32, _> = redis::cmd("INCR").arg("string").query(conn); | ||||
|     let result: RedisResult<i32> = redis::cmd("INCR").arg("string").query(conn); | ||||
|     assert!(result.is_err()); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_hash_operations(conn: &mut Connection) { | ||||
|     // Test HSET | ||||
|     cleanup_keys(conn).await; | ||||
|     let result: i32 = conn.hset("hash", "field1", "value1").unwrap(); | ||||
|     assert_eq!(result, 1); // 1 new field | ||||
|  | ||||
|     // Test HGET | ||||
|     assert_eq!(result, 1); | ||||
|     let result: String = conn.hget("hash", "field1").unwrap(); | ||||
|     assert_eq!(result, "value1"); | ||||
|  | ||||
|     // Test HSET multiple fields | ||||
|     let _: () = conn.hset_multiple("hash", &[("field2", "value2"), ("field3", "value3")]).unwrap(); | ||||
|  | ||||
|     // Test HGETALL | ||||
|     let _: () = conn.hset("hash", "field2", "value2").unwrap(); | ||||
|     let _: () = conn.hset("hash", "field3", "value3").unwrap(); | ||||
|     let result: std::collections::HashMap<String, String> = conn.hgetall("hash").unwrap(); | ||||
|     assert_eq!(result.len(), 3); | ||||
|     assert_eq!(result.get("field1").unwrap(), "value1"); | ||||
|     assert_eq!(result.get("field2").unwrap(), "value2"); | ||||
|     assert_eq!(result.get("field3").unwrap(), "value3"); | ||||
|  | ||||
|     // Test HLEN | ||||
|     let result: i32 = conn.hlen("hash").unwrap(); | ||||
|     assert_eq!(result, 3); | ||||
|  | ||||
|     // Test HEXISTS | ||||
|     let result: bool = conn.hexists("hash", "field1").unwrap(); | ||||
|     assert_eq!(result, true); | ||||
|  | ||||
|     let result: bool = conn.hexists("hash", "noexist").unwrap(); | ||||
|     assert_eq!(result, false); | ||||
|  | ||||
|     // Test HDEL | ||||
|     let result: i32 = conn.hdel("hash", "field1").unwrap(); | ||||
|     assert_eq!(result, 1); | ||||
|  | ||||
|     // Test HKEYS | ||||
|     let mut result: Vec<String> = conn.hkeys("hash").unwrap(); | ||||
|     result.sort(); | ||||
|     assert_eq!(result, vec!["field2", "field3"]); | ||||
|  | ||||
|     // Test HVALS | ||||
|     let mut result: Vec<String> = conn.hvals("hash").unwrap(); | ||||
|     result.sort(); | ||||
|     assert_eq!(result, vec!["value2", "value3"]); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_expiration(conn: &mut Connection) { | ||||
|     // Test SETEX (expire in 1 second) | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = conn.set_ex("expkey", "value", 1).unwrap(); | ||||
|  | ||||
|     // Test TTL | ||||
|     let result: i32 = conn.ttl("expkey").unwrap(); | ||||
|     assert!(result == 1 || result == 0); // Should be 1 or 0 seconds | ||||
|  | ||||
|     // Test EXISTS | ||||
|     assert!(result == 1 || result == 0); | ||||
|     let result: bool = conn.exists("expkey").unwrap(); | ||||
|     assert_eq!(result, true); | ||||
|  | ||||
|     // Wait for expiration | ||||
|     sleep(Duration::from_millis(1100)).await; | ||||
|  | ||||
|     // Test GET after expiration | ||||
|     let result: Option<String> = conn.get("expkey").unwrap(); | ||||
|     assert_eq!(result, None); | ||||
|  | ||||
|     // Test TTL after expiration | ||||
|     let result: i32 = conn.ttl("expkey").unwrap(); | ||||
|     assert_eq!(result, -2); // Key doesn't exist | ||||
|  | ||||
|     // Test EXISTS after expiration | ||||
|     assert_eq!(result, -2); | ||||
|     let result: bool = conn.exists("expkey").unwrap(); | ||||
|     assert_eq!(result, false); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_scan_operations(conn: &mut Connection) { | ||||
|     // Set up test data | ||||
|     cleanup_keys(conn).await; | ||||
|     for i in 0..5 { | ||||
|         let _: () = conn.set(format!("key{}", i), format!("value{}", i)).unwrap(); | ||||
|     } | ||||
|  | ||||
|     // Test SCAN | ||||
|     let result: (u64, Vec<String>) = redis::cmd("SCAN") | ||||
|         .arg(0) | ||||
|         .arg("MATCH") | ||||
|         .arg("*") | ||||
|         .arg("key*") | ||||
|         .arg("COUNT") | ||||
|         .arg(10) | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|  | ||||
|     let (cursor, keys) = result; | ||||
|     assert_eq!(cursor, 0); // Should complete in one scan | ||||
|     assert_eq!(cursor, 0); | ||||
|     assert_eq!(keys.len(), 5); | ||||
|  | ||||
|     // Test KEYS | ||||
|     let mut result: Vec<String> = redis::cmd("KEYS").arg("*").query(conn).unwrap(); | ||||
|     result.sort(); | ||||
|     assert_eq!(result, vec!["key0", "key1", "key2", "key3", "key4"]); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_scan_with_count(conn: &mut Connection) { | ||||
|     // Clean up previous keys | ||||
|     let keys: Vec<String> = redis::cmd("KEYS").arg("scan_key*").query(conn).unwrap(); | ||||
|     if !keys.is_empty() { | ||||
|         let _: () = redis::cmd("DEL").arg(keys).query(conn).unwrap(); | ||||
|     } | ||||
|  | ||||
|     // Set up test data | ||||
|     cleanup_keys(conn).await; | ||||
|     for i in 0..15 { | ||||
|         let _: () = conn.set(format!("scan_key{}", i), i).unwrap(); | ||||
|     } | ||||
|  | ||||
|     let mut cursor = 0; | ||||
|     let mut all_keys = std::collections::HashSet::new(); | ||||
|  | ||||
|     // First SCAN | ||||
|     let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN") | ||||
|         .arg(cursor) | ||||
|         .arg("MATCH") | ||||
|         .arg("scan_key*") | ||||
|         .arg("COUNT") | ||||
|         .arg(5) | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|  | ||||
|     assert_ne!(next_cursor, 0); | ||||
|     assert_eq!(keys.len(), 5); | ||||
|     for key in keys { | ||||
|         all_keys.insert(key); | ||||
|     loop { | ||||
|         let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN") | ||||
|             .arg(cursor) | ||||
|             .arg("MATCH") | ||||
|             .arg("scan_key*") | ||||
|             .arg("COUNT") | ||||
|             .arg(5) | ||||
|             .query(conn) | ||||
|             .unwrap(); | ||||
|         for key in keys { | ||||
|             all_keys.insert(key); | ||||
|         } | ||||
|         cursor = next_cursor; | ||||
|         if cursor == 0 { | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|     cursor = next_cursor; | ||||
|  | ||||
|     // Second SCAN | ||||
|     let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN") | ||||
|         .arg(cursor) | ||||
|         .arg("MATCH") | ||||
|         .arg("scan_key*") | ||||
|         .arg("COUNT") | ||||
|         .arg(5) | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|  | ||||
|     assert_ne!(next_cursor, 0); | ||||
|     assert_eq!(keys.len(), 5); | ||||
|     for key in keys { | ||||
|         all_keys.insert(key); | ||||
|     } | ||||
|     cursor = next_cursor; | ||||
|  | ||||
|     // Final SCAN | ||||
|     let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN") | ||||
|         .arg(cursor) | ||||
|         .arg("MATCH") | ||||
|         .arg("scan_key*") | ||||
|         .arg("COUNT") | ||||
|         .arg(5) | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|  | ||||
|     assert_eq!(next_cursor, 0); | ||||
|     assert_eq!(keys.len(), 5); | ||||
|     for key in keys { | ||||
|         all_keys.insert(key); | ||||
|     } | ||||
|  | ||||
|     assert_eq!(all_keys.len(), 15); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_hscan_operations(conn: &mut Connection) { | ||||
|     // Set up hash data | ||||
|     cleanup_keys(conn).await; | ||||
|     for i in 0..3 { | ||||
|         let _: () = conn.hset("testhash", format!("field{}", i), format!("value{}", i)).unwrap(); | ||||
|     } | ||||
|  | ||||
|     // Test HSCAN | ||||
|     let result: (u64, Vec<String>) = redis::cmd("HSCAN") | ||||
|         .arg("testhash") | ||||
|         .arg(0) | ||||
| @@ -364,64 +266,56 @@ async fn test_hscan_operations(conn: &mut Connection) { | ||||
|         .arg(10) | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|  | ||||
|     let (cursor, fields) = result; | ||||
|     assert_eq!(cursor, 0); // Should complete in one scan | ||||
|     assert_eq!(fields.len(), 6); // 3 field-value pairs = 6 elements | ||||
|     assert_eq!(cursor, 0); | ||||
|     assert_eq!(fields.len(), 6); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_transaction_operations(conn: &mut Connection) { | ||||
|     // Test MULTI/EXEC | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = redis::cmd("MULTI").query(conn).unwrap(); | ||||
|     let _: () = redis::cmd("SET").arg("key1").arg("value1").query(conn).unwrap(); | ||||
|     let _: () = redis::cmd("SET").arg("key2").arg("value2").query(conn).unwrap(); | ||||
|     let _: Vec<String> = redis::cmd("EXEC").query(conn).unwrap(); | ||||
|  | ||||
|     // Verify commands were executed | ||||
|     let result: String = conn.get("key1").unwrap(); | ||||
|     assert_eq!(result, "value1"); | ||||
|  | ||||
|     let result: String = conn.get("key2").unwrap(); | ||||
|     assert_eq!(result, "value2"); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_discard_transaction(conn: &mut Connection) { | ||||
|     // Test MULTI/DISCARD | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = redis::cmd("MULTI").query(conn).unwrap(); | ||||
|     let _: () = redis::cmd("SET").arg("discard").arg("value").query(conn).unwrap(); | ||||
|     let _: () = redis::cmd("DISCARD").query(conn).unwrap(); | ||||
|  | ||||
|     // Verify command was not executed | ||||
|     let result: Option<String> = conn.get("discard").unwrap(); | ||||
|     assert_eq!(result, None); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_type_command(conn: &mut Connection) { | ||||
|     // Test string type | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = conn.set("string", "value").unwrap(); | ||||
|     let result: String = redis::cmd("TYPE").arg("string").query(conn).unwrap(); | ||||
|     assert_eq!(result, "string"); | ||||
|  | ||||
|     // Test hash type | ||||
|     let _: () = conn.hset("hash", "field", "value").unwrap(); | ||||
|     let result: String = redis::cmd("TYPE").arg("hash").query(conn).unwrap(); | ||||
|     assert_eq!(result, "hash"); | ||||
|  | ||||
|     // Test non-existent key | ||||
|     let result: String = redis::cmd("TYPE").arg("noexist").query(conn).unwrap(); | ||||
|     assert_eq!(result, "none"); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_config_commands(conn: &mut Connection) { | ||||
|     // Test CONFIG GET databases | ||||
|     cleanup_keys(conn).await; | ||||
|     let result: Vec<String> = redis::cmd("CONFIG") | ||||
|         .arg("GET") | ||||
|         .arg("databases") | ||||
|         .query(conn) | ||||
|         .unwrap(); | ||||
|     assert_eq!(result, vec!["databases", "16"]); | ||||
|  | ||||
|     // Test CONFIG GET dir | ||||
|     let result: Vec<String> = redis::cmd("CONFIG") | ||||
|         .arg("GET") | ||||
|         .arg("dir") | ||||
| @@ -429,33 +323,28 @@ async fn test_config_commands(conn: &mut Connection) { | ||||
|         .unwrap(); | ||||
|     assert_eq!(result[0], "dir"); | ||||
|     assert!(result[1].contains("/tmp/herodb_test_")); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_info_command(conn: &mut Connection) { | ||||
|     // Test INFO | ||||
|     cleanup_keys(conn).await; | ||||
|     let result: String = redis::cmd("INFO").query(conn).unwrap(); | ||||
|     assert!(result.contains("redis_version")); | ||||
|  | ||||
|     // Test INFO replication | ||||
|     let result: String = redis::cmd("INFO").arg("replication").query(conn).unwrap(); | ||||
|     assert!(result.contains("role:master")); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
|  | ||||
| async fn test_error_handling(conn: &mut Connection) { | ||||
|     // Test WRONGTYPE error - try to use hash command on string | ||||
|     cleanup_keys(conn).await; | ||||
|     let _: () = conn.set("string", "value").unwrap(); | ||||
|     let result: Result<String, _> = conn.hget("string", "field"); | ||||
|     let result: RedisResult<String> = conn.hget("string", "field"); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test unknown command | ||||
|     let result: Result<String, _> = redis::cmd("UNKNOWN").query(conn); | ||||
|     let result: RedisResult<String> = redis::cmd("UNKNOWN").query(conn); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test EXEC without MULTI | ||||
|     let result: Result<Vec<String>, _> = redis::cmd("EXEC").query(conn); | ||||
|     let result: RedisResult<Vec<String>> = redis::cmd("EXEC").query(conn); | ||||
|     assert!(result.is_err()); | ||||
|  | ||||
|     // Test DISCARD without MULTI | ||||
|     let result: Result<(), _> = redis::cmd("DISCARD").query(conn); | ||||
|     let result: RedisResult<()> = redis::cmd("DISCARD").query(conn); | ||||
|     assert!(result.is_err()); | ||||
|     cleanup_keys(conn).await; | ||||
| } | ||||
| @@ -20,6 +20,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | ||||
|         dir: test_dir, | ||||
|         port, | ||||
|         debug: true, | ||||
|         databases: 16, | ||||
|     }; | ||||
|      | ||||
|     let server = Server::new(option).await; | ||||
|   | ||||
| @@ -22,6 +22,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | ||||
|         dir: test_dir, | ||||
|         port, | ||||
|         debug: true, | ||||
|         databases: 16, | ||||
|     }; | ||||
|      | ||||
|     let server = Server::new(option).await; | ||||
|   | ||||
| @@ -20,6 +20,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) { | ||||
|         dir: test_dir, | ||||
|         port, | ||||
|         debug: false, | ||||
|         databases: 16, | ||||
|     }; | ||||
|      | ||||
|     let server = Server::new(option).await; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user