From 621074becaba5f506c96c0a8c7c9c4b6d7cd472b Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 22 Aug 2026 01:47:20 +0000 Subject: [PATCH] fix(redis): allow db0 commands without SELECT support Closes #6876 --- crates/dbx-core/src/db/redis_driver.rs | 106 +++++++++++++++++++++++++ crates/dbx-core/src/redis_ops.rs | 3 +- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index 272dbad476..172e6ef1ef 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -1057,6 +1057,33 @@ where redis::cmd("SELECT").arg(db).query_async(con).await.map_err(|e| e.to_string()) } +pub async fn execute_console_command( + con: &mut C, + db: u32, + command_text: &str, + skip_safety_check: bool, +) -> Result +where + C: ConnectionLike + Send + Sync + Unpin, +{ + let selection: redis::RedisResult<()> = redis::cmd("SELECT").arg(db).query_async(con).await; + if let Err(error) = selection { + if !is_unavailable_db_zero_select(&error, db) { + return Err(error.to_string()); + } + } + execute_command(con, command_text, skip_safety_check).await +} + +fn is_unavailable_db_zero_select(error: &redis::RedisError, db: u32) -> bool { + if db != 0 || error.kind() != redis::ErrorKind::ResponseError { + return false; + } + let detail = error.detail().unwrap_or_default().trim_start(); + let prefix_len = "unknown command".len(); + detail.get(..prefix_len).is_some_and(|prefix| prefix.eq_ignore_ascii_case("unknown command")) +} + pub fn ensure_cluster_db(db: u32) -> Result<(), String> { if db == 0 { Ok(()) @@ -3692,6 +3719,85 @@ mod tests { ) } + #[tokio::test] + async fn db0_console_command_continues_when_select_is_unavailable() { + let select_error = redis::RedisError::from(( + redis::ErrorKind::ResponseError, + "An error was signalled by the server", + "unknown command 'select', with args beginning with: '0'".to_string(), + )); + let mut con = FakeRedisConnection::with_results(vec![Err(select_error), Ok(bulk("Ada"))]); + + let result = super::execute_console_command(&mut con, 0, "GET name", false).await.unwrap(); + + assert_eq!(result.command, "GET"); + assert_eq!(result.value, serde_json::json!("Ada")); + assert_eq!(con.command_count("SELECT"), 1); + assert_eq!(con.command_count("GET"), 1); + } + + #[tokio::test] + async fn console_command_keeps_supported_db_zero_selection() { + let mut con = FakeRedisConnection::new(vec![RedisRawValue::Okay, bulk("Ada")]); + + let result = super::execute_console_command(&mut con, 0, "GET name", false).await.unwrap(); + + assert_eq!(result.value, serde_json::json!("Ada")); + assert_eq!(con.command_count("SELECT"), 1); + assert_eq!(con.command_count("GET"), 1); + } + + #[tokio::test] + async fn console_command_does_not_ignore_unavailable_select_for_nonzero_db() { + let select_error = redis::RedisError::from(( + redis::ErrorKind::ResponseError, + "An error was signalled by the server", + "unknown command 'select', with args beginning with: '1'".to_string(), + )); + let mut con = FakeRedisConnection::with_results(vec![Err(select_error), Ok(bulk("wrong db"))]); + + let error = super::execute_console_command(&mut con, 1, "GET name", false).await.unwrap_err(); + + assert!(error.contains("unknown command")); + assert_eq!(con.command_count("SELECT"), 1); + assert_eq!(con.command_count("GET"), 0); + } + + #[tokio::test] + async fn console_command_propagates_non_compatibility_select_errors() { + let errors = vec![ + redis::RedisError::from(( + redis::ErrorKind::ResponseError, + "An error was signalled by the server", + "invalid DB index".to_string(), + )), + noperm("SELECT"), + redis::RedisError::from((redis::ErrorKind::AuthenticationFailed, "Authentication failed")), + redis::RedisError::from((redis::ErrorKind::ParseError, "Invalid response")), + redis::RedisError::from(std::io::Error::new(std::io::ErrorKind::ConnectionReset, "connection reset")), + ]; + + for select_error in errors { + let mut con = FakeRedisConnection::with_results(vec![Err(select_error), Ok(bulk("must not run"))]); + + assert!(super::execute_console_command(&mut con, 0, "GET name", false).await.is_err()); + assert_eq!(con.command_count("SELECT"), 1); + assert_eq!(con.command_count("GET"), 0); + } + } + + #[tokio::test] + async fn strict_database_selection_keeps_unavailable_select_error() { + let select_error = redis::RedisError::from(( + redis::ErrorKind::ResponseError, + "An error was signalled by the server", + "unknown command 'select'".to_string(), + )); + let mut con = FakeRedisConnection::with_results(vec![Err(select_error)]); + + assert!(super::select_db(&mut con, 0).await.is_err()); + } + #[test] fn parses_stream_entries() { let raw = RedisRawValue::Array(vec![RedisRawValue::Array(vec![ diff --git a/crates/dbx-core/src/redis_ops.rs b/crates/dbx-core/src/redis_ops.rs index 16f62c3207..287500af28 100644 --- a/crates/dbx-core/src/redis_ops.rs +++ b/crates/dbx-core/src/redis_ops.rs @@ -1020,8 +1020,7 @@ pub async fn redis_execute_command_core( PoolKind::Redis(redis) => match redis { RedisConnection::Direct(con) => { let mut con = con.lock().await; - redis_driver::select_db(&mut *con, db).await?; - redis_driver::execute_command(&mut *con, command, skip_safety_check).await + redis_driver::execute_console_command(&mut *con, db, command, skip_safety_check).await } RedisConnection::Cluster(cluster) => { redis_driver::ensure_cluster_db(db)?;