diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue index f1a2810124..390ba43e35 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue @@ -875,6 +875,38 @@ function onKeyDeleted(keyRaw: string) { removeKnownKey(keyRaw); } +function onKeyRenamed(oldKeyRaw: string, newKeyRaw: string, newKeyDisplay: string) { + connectionStore.invalidateCompletionCache(props.connectionId, String(props.db)); + if (isSearchMode.value) { + void loadKeys(); + return; + } + + const previous = flatKeys.value.find((key) => key.key_raw === oldKeyRaw); + if (!previous) { + void loadKeys(); + return; + } + + loadedKeyRaws.delete(oldKeyRaw); + loadedKeyRaws.add(newKeyRaw); + flatKeys.value = flatKeys.value.map((key) => (key.key_raw === oldKeyRaw ? { ...key, key_raw: newKeyRaw, key_display: newKeyDisplay } : key)); + if (selectedKeyRaw.value === oldKeyRaw) selectedKeyRaw.value = newKeyRaw; + if (checkedKeys.value.has(oldKeyRaw)) { + const nextCheckedKeys = new Set(checkedKeys.value); + nextCheckedKeys.delete(oldKeyRaw); + nextCheckedKeys.add(newKeyRaw); + checkedKeys.value = nextCheckedKeys; + } + if (useFlatKeySearchRows.value) { + treeKeys.value = []; + treeIndex = null; + refreshSelectedGroupLeafCounts(); + } else { + rebuildTree(false); + } +} + function redisValueToKeyInfo(value: RedisValue): RedisKeyInfo { return { key_display: value.key_display, @@ -2093,7 +2125,19 @@ defineExpose({ focusSearch, insertCommand, executeCommand: executeAiCommand }); - +
{{ t("redis.selectKeyForDetail") }}
diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index 785cb79c2a..d77939ed82 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -55,6 +55,7 @@ import TextContentSearchBar from "@/components/common/TextContentSearchBar.vue"; import { decodeJsonUnicodeEscapes, formatJsonSource, mapDisplayToRaw } from "@/lib/common/safeJsonFormat"; import { unixSecondsToCalendarDateTime } from "@/components/ui/date-time-picker/dateTimePicker"; import { applyRedisExpiryPolicy, type RedisExpiryMode, redisExpiryModeForTtl, validateRedisExpiry } from "@/lib/redis/redisExpiry"; +import { redisKeyRawToText, redisKeyTextToDisplay, redisKeyTextToRaw } from "@/lib/redis/redisCommandSession"; const { t, locale } = useI18n(); const { toast } = useToast(); @@ -74,7 +75,7 @@ const redisExpiryTransport = { setExpireAt: api.redisSetExpireAt, }; -const emit = defineEmits<{ deleted: [keyRaw: string]; loaded: [value: RedisValue] }>(); +const emit = defineEmits<{ deleted: [keyRaw: string]; loaded: [value: RedisValue]; renamed: [oldKeyRaw: string, newKeyRaw: string, newKeyDisplay: string] }>(); const REDIS_JSON_WRAP_STORAGE_KEY = "dbx-redis-json-word-wrap"; const REDIS_JSON_UNICODE_MODE_STORAGE_KEY = "dbx-redis-json-unicode-mode"; @@ -90,6 +91,12 @@ const REDIS_STREAM_MIN_ROW_HEIGHT = 96; const data = ref(null); const loading = ref(false); const loadingMore = ref(false); +const showRenameKeyDialog = ref(false); +const renamingKey = ref(false); +const renameKeyName = ref(""); +const renameKeyError = ref(""); +const renameKeyInitialName = ref(""); +const renameKeyInitialRaw = ref(""); let loadRequestId = 0; const streamTab = ref<"entries" | "groups">("entries"); const streamEntries = ref([]); @@ -1474,6 +1481,50 @@ function requestDeleteKey() { showDeleteConfirm.value = true; } +function openRenameKeyDialog() { + if (!data.value || renamingKey.value) return; + // Display text escapes backslashes and binary bytes. Prefer the original + // UTF-8 text when available so confirming an unchanged name is a true no-op. + renameKeyName.value = redisKeyRawToText(props.keyRaw) ?? data.value.key_display; + renameKeyInitialName.value = renameKeyName.value; + renameKeyInitialRaw.value = props.keyRaw; + renameKeyError.value = ""; + showRenameKeyDialog.value = true; +} + +function handleRenameKeyDialogOpenChange(open: boolean) { + if (renamingKey.value) return; + showRenameKeyDialog.value = open; + if (!open) renameKeyError.value = ""; +} + +async function renameKey() { + if (renamingKey.value) return; + if (!renameKeyName.value) { + renameKeyError.value = t("redis.renameKeyNameRequired"); + return; + } + + const newKeyRaw = renameKeyName.value === renameKeyInitialName.value ? renameKeyInitialRaw.value : redisKeyTextToRaw(renameKeyName.value); + if (newKeyRaw === props.keyRaw) { + showRenameKeyDialog.value = false; + return; + } + + renamingKey.value = true; + renameKeyError.value = ""; + try { + await api.redisRenameKey(props.connectionId, props.db, props.keyRaw, newKeyRaw); + toast(t("redis.renameKeySuccess", { oldName: props.keyDisplay, newName: renameKeyName.value }), 2000); + showRenameKeyDialog.value = false; + emit("renamed", props.keyRaw, newKeyRaw, redisKeyTextToDisplay(renameKeyName.value)); + } catch (error) { + renameKeyError.value = error instanceof Error ? error.message : String(error); + } finally { + renamingKey.value = false; + } +} + async function copyValue() { if (!data.value) return; // Copy the decompressed text while the Decompressed view is showing it. @@ -2497,6 +2548,7 @@ defineExpose({ focusSearch }); + @@ -3184,6 +3236,27 @@ defineExpose({ focusSearch }); + + + + {{ t("redis.renameKey") }} + + +

{{ renameKeyError }}

+ + + + +
+
+ diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 5655cbbe78..1a305baa8d 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -4409,6 +4409,10 @@ export default { createValuePlaceholder: "value", createKeySubmit: "Create", createKeyNameRequired: "Enter a key name", + renameKey: "Rename Key", + renameKeyName: "Key name", + renameKeyNameRequired: "Enter a key name", + renameKeySuccess: 'Renamed "{oldName}" to "{newName}"', createFieldRequired: "Enter a hash field", createScoreInvalid: "Enter a valid score", jsonModuleNotAvailable: "ReJSON module is not loaded on this Redis server", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index fe7a9f4788..7b63786d33 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -4247,6 +4247,10 @@ export default withEnglishFallback({ jsonModuleNotAvailable: "El módulo ReJSON no está cargado en este servidor Redis", createKeySubmit: "Crear", createKeyNameRequired: "Ingresa una clave", + renameKey: "Renombrar clave", + renameKeyName: "Nombre de la clave", + renameKeyNameRequired: "Ingresa un nombre de clave", + renameKeySuccess: 'Clave "{oldName}" renombrada a "{newName}"', createFieldRequired: "Ingresa un campo hash", createScoreInvalid: "Ingresa una puntuación válida", fieldRequired: "Ingresa un nombre de campo", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 9ce5d63b59..1fdb3f0d53 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -4244,6 +4244,10 @@ export default withEnglishFallback({ createValuePlaceholder: "valore", createKeySubmit: "Crea", createKeyNameRequired: "Inserisci un nome chiave", + renameKey: "Rinomina chiave", + renameKeyName: "Nome chiave", + renameKeyNameRequired: "Inserisci un nome chiave", + renameKeySuccess: 'Chiave "{oldName}" rinominata in "{newName}"', createFieldRequired: "Inserisci un campo hash", createScoreInvalid: "Inserisci un punteggio valido", jsonModuleNotAvailable: "Il modulo ReJSON non è caricato su questo server Redis", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 5583be39c5..b23693ce30 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -4274,6 +4274,10 @@ export default withEnglishFallback({ createValuePlaceholder: "value", createKeySubmit: "作成", createKeyNameRequired: "キー名を入力してください", + renameKey: "キー名を変更", + renameKeyName: "キー名", + renameKeyNameRequired: "キー名を入力してください", + renameKeySuccess: "「{oldName}」を「{newName}」に変更しました", createFieldRequired: "ハッシュフィールドを入力してください", createScoreInvalid: "有効なスコアを入力してください", jsonModuleNotAvailable: "このRedisサーバーにReJSONモジュールがロードされていません", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 8e91dc6f97..c51080ed44 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -3893,6 +3893,10 @@ export default withEnglishFallback({ createValuePlaceholder: "값", createKeySubmit: "만들기", createKeyNameRequired: "키 이름을 입력하세요", + renameKey: "키 이름 변경", + renameKeyName: "키 이름", + renameKeyNameRequired: "키 이름을 입력하세요", + renameKeySuccess: '"{oldName}"을(를) "{newName}"(으)로 변경했습니다', createFieldRequired: "해시 필드를 입력하세요", createScoreInvalid: "올바른 점수를 입력하세요", jsonModuleNotAvailable: "이 Redis 서버에 ReJSON 모듈이 로드되지 않았습니다", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 06db6ec04d..0958249f3e 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -4247,6 +4247,10 @@ export default withEnglishFallback({ jsonModuleNotAvailable: "Módulo ReJSON não está carregado neste servidor Redis", createKeySubmit: "Criar", createKeyNameRequired: "Insira um nome de chave", + renameKey: "Renomear chave", + renameKeyName: "Nome da chave", + renameKeyNameRequired: "Insira um nome de chave", + renameKeySuccess: 'Chave "{oldName}" renomeada para "{newName}"', createFieldRequired: "Insira um campo de hash", createScoreInvalid: "Insira uma pontuação válida", fieldRequired: "Insira um nome de campo", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 819cb5e92a..8a900af319 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -4396,6 +4396,10 @@ export default withEnglishFallback({ createValuePlaceholder: "value", createKeySubmit: "新增", createKeyNameRequired: "请输入 key 名称", + renameKey: "重命名 Key", + renameKeyName: "Key 名称", + renameKeyNameRequired: "请输入 Key 名称", + renameKeySuccess: "已将「{oldName}」重命名为「{newName}」", createFieldRequired: "请输入 hash 字段", createScoreInvalid: "请输入有效分数", jsonModuleNotAvailable: "该 Redis 服务器未加载 ReJSON 模块", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 1898823c07..100a7e632f 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -3573,6 +3573,10 @@ export default withEnglishFallback({ createValuePlaceholder: "value", createKeySubmit: "建立", createKeyNameRequired: "請輸入 key 名稱", + renameKey: "重新命名 Key", + renameKeyName: "Key 名稱", + renameKeyNameRequired: "請輸入 Key 名稱", + renameKeySuccess: "已將「{oldName}」重新命名為「{newName}」", createFieldRequired: "請輸入 hash 欄位", createScoreInvalid: "請輸入有效分數", jsonModuleNotAvailable: "該 Redis 伺服器未載入 ReJSON 模組", diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index 597d180df9..b067cc4f26 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -443,6 +443,7 @@ export const redisGetStreamConsumers = forward("redisGetStreamConsumers"); export const redisGetStreamPending = forward("redisGetStreamPending"); export const redisSetString = forward("redisSetString"); export const redisDeleteKey = forward("redisDeleteKey"); +export const redisRenameKey = forward("redisRenameKey"); export const redisHashSet = forward("redisHashSet"); export const redisHashDel = forward("redisHashDel"); export const redisHashFieldSetTtl = forward("redisHashFieldSetTtl"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index 7a15420699..51ea0708c1 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -2535,6 +2535,10 @@ export async function redisDeleteKey(connectionId: string, db: number, keyRaw: s return post("/api/redis/delete-key", { connectionId, db, keyRaw }); } +export async function redisRenameKey(connectionId: string, db: number, keyRaw: string, newKeyRaw: string): Promise { + return post("/api/redis/rename-key", { connectionId, db, keyRaw, newKeyRaw }); +} + export async function redisHashSet(connectionId: string, db: number, keyRaw: string, field: string, value: string, ttl?: number): Promise { return post("/api/redis/hash-set", { connectionId, diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index e2c1b4d361..5e3594c777 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -2342,6 +2342,10 @@ export async function redisDeleteKey(connectionId: string, db: number, keyRaw: s return invoke("redis_delete_key", { connectionId, db, keyRaw }); } +export async function redisRenameKey(connectionId: string, db: number, keyRaw: string, newKeyRaw: string): Promise { + return invoke("redis_rename_key", { connectionId, db, keyRaw, newKeyRaw }); +} + export async function redisHashSet(connectionId: string, db: number, keyRaw: string, field: string, value: string, ttl?: number): Promise { return invoke("redis_hash_set", { connectionId, diff --git a/apps/desktop/src/lib/redis/redisCommandSession.spec.ts b/apps/desktop/src/lib/redis/redisCommandSession.spec.ts new file mode 100644 index 0000000000..a8ff075087 --- /dev/null +++ b/apps/desktop/src/lib/redis/redisCommandSession.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { redisKeyRawToText, redisKeyTextToDisplay, redisKeyTextToRaw } from "./redisCommandSession"; + +describe("Redis key text encoding", () => { + it("round-trips UTF-8 keys without treating backslashes as display escapes", () => { + const key = "cache\\session"; + const raw = redisKeyTextToRaw(key); + + expect(redisKeyRawToText(raw)).toBe(key); + expect(redisKeyTextToDisplay(key)).toBe("cache\\\\session"); + }); + + it("does not coerce binary key bytes into a text key name", () => { + expect(redisKeyRawToText("/w==")).toBeNull(); + }); +}); diff --git a/apps/desktop/src/lib/redis/redisCommandSession.ts b/apps/desktop/src/lib/redis/redisCommandSession.ts index 5d15f38843..86ce1159e2 100644 --- a/apps/desktop/src/lib/redis/redisCommandSession.ts +++ b/apps/desktop/src/lib/redis/redisCommandSession.ts @@ -20,3 +20,17 @@ export function redisKeyTextToRaw(key: string): string { } return btoa(binary); } + +/** Returns null when a Redis key contains bytes that cannot be represented in a text input. */ +export function redisKeyRawToText(keyRaw: string): string | null { + try { + const bytes = Uint8Array.from(atob(keyRaw), (character) => character.charCodeAt(0)); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + return null; + } +} + +export function redisKeyTextToDisplay(key: string): string { + return key.replaceAll("\\", "\\\\"); +} diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index b114e94d36..857422c5b3 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -2862,6 +2862,20 @@ where redis::cmd("DEL").arg(key).query_async::<()>(con).await.map_err(|e| e.to_string()) } +/// Rename a key without overwriting an existing destination. +pub async fn rename_key(con: &mut C, key: &[u8], new_key: &[u8]) -> Result<(), String> +where + C: ConnectionLike + Send + Sync + Unpin, +{ + let renamed = + redis::cmd("RENAMENX").arg(key).arg(new_key).query_async::(con).await.map_err(|e| e.to_string())?; + if renamed { + Ok(()) + } else { + Err("Target key already exists".to_string()) + } +} + async fn apply_expire_if_needed(con: &mut C, key: &[u8], ttl: Option) -> Result<(), String> where C: ConnectionLike + Send + Sync + Unpin, @@ -4177,6 +4191,27 @@ mod tests { assert!(!con.commands[0].contains("\r\nEXPIRE\r\n")); } + #[tokio::test] + async fn rename_key_uses_renamenx_without_overwriting_the_destination() { + let mut con = FakeRedisConnection::new(vec![RedisRawValue::Int(1)]); + + super::rename_key(&mut con, b"session:old", b"session:new").await.unwrap(); + + assert_eq!(con.commands.len(), 1); + assert!(con.commands[0].contains("\r\nRENAMENX\r\n")); + assert!(con.commands[0].contains("\r\nsession:old\r\n")); + assert!(con.commands[0].contains("\r\nsession:new\r\n")); + } + + #[tokio::test] + async fn rename_key_reports_a_destination_conflict() { + let mut con = FakeRedisConnection::new(vec![RedisRawValue::Int(0)]); + + let error = super::rename_key(&mut con, b"session:old", b"session:new").await.unwrap_err(); + + assert_eq!(error, "Target key already exists"); + } + #[tokio::test] async fn set_expire_at_uses_expireat_with_the_unix_timestamp() { let mut con = FakeRedisConnection::new(vec![RedisRawValue::Int(1)]); diff --git a/crates/dbx-core/src/redis_ops.rs b/crates/dbx-core/src/redis_ops.rs index 2702d12d94..5549343b22 100644 --- a/crates/dbx-core/src/redis_ops.rs +++ b/crates/dbx-core/src/redis_ops.rs @@ -366,6 +366,38 @@ pub async fn redis_delete_key_in_db_core( } } +pub async fn redis_rename_key_in_db_core( + state: &AppState, + connection_id: &str, + db: u32, + key_raw: &str, + new_key_raw: &str, +) -> Result<(), String> { + ensure_redis_pool(state, connection_id).await?; + let connections = state.connections.read().await; + let pool = connections.get(connection_id).ok_or("Connection not found")?; + let key = redis_driver::redis_key_raw_to_bytes(key_raw)?; + let new_key = redis_driver::redis_key_raw_to_bytes(new_key_raw)?; + if key == new_key { + return Ok(()); + } + match pool { + PoolKind::Redis(redis) => match redis { + RedisConnection::Direct(con) => { + let mut con = con.lock().await; + redis_driver::select_db(&mut *con, db).await?; + redis_driver::rename_key(&mut *con, &key, &new_key).await + } + RedisConnection::Cluster(cluster) => { + redis_driver::ensure_cluster_db(db)?; + let mut con = redis_driver::cluster_key_connection(cluster, &key).await?; + redis_driver::rename_key(&mut con, &key, &new_key).await + } + }, + _ => Err("Not a Redis connection".to_string()), + } +} + pub async fn redis_hash_set_core( state: &AppState, connection_id: &str, diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index fe3c55e7d5..59c253a04f 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -564,6 +564,7 @@ async fn main() { .route("/redis/load-more", post(routes::redis::load_more)) .route("/redis/set-string", post(routes::redis::set_string)) .route("/redis/delete-key", post(routes::redis::delete_key)) + .route("/redis/rename-key", post(routes::redis::rename_key)) .route("/redis/hash-set", post(routes::redis::hash_set)) .route("/redis/hash-del", post(routes::redis::hash_del)) .route("/redis/hash-field-set-ttl", post(routes::redis::hash_field_set_ttl)) diff --git a/crates/dbx-web/src/routes/redis.rs b/crates/dbx-web/src/routes/redis.rs index 0fec7f0c23..18d2953d51 100644 --- a/crates/dbx-web/src/routes/redis.rs +++ b/crates/dbx-web/src/routes/redis.rs @@ -76,6 +76,15 @@ pub struct RedisKeyRequest { pub key_raw: String, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RedisRenameKeyRequest { + pub connection_id: String, + pub db: u32, + pub key_raw: String, + pub new_key_raw: String, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct RedisStreamEntriesRequest { @@ -484,6 +493,23 @@ pub async fn delete_key( Ok(Json(())) } +pub async fn rename_key( + State(state): State>, + Json(req): Json, +) -> Result, AppError> { + ensure_writable(&state.app, &req.connection_id, "RENAMENX").await?; + dbx_core::redis_ops::redis_rename_key_in_db_core( + &state.app, + &req.connection_id, + req.db, + &req.key_raw, + &req.new_key_raw, + ) + .await + .map_err(AppError::from)?; + Ok(Json(())) +} + pub async fn hash_set( State(state): State>, Json(req): Json, diff --git a/src-tauri/src/commands/redis_cmd.rs b/src-tauri/src/commands/redis_cmd.rs index 05b53586bc..96157ec07e 100644 --- a/src-tauri/src/commands/redis_cmd.rs +++ b/src-tauri/src/commands/redis_cmd.rs @@ -175,6 +175,18 @@ pub async fn redis_delete_key( dbx_core::redis_ops::redis_delete_key_in_db_core(&state, &connection_id, db, &key_raw).await } +#[tauri::command] +pub async fn redis_rename_key( + state: State<'_, Arc>, + connection_id: String, + db: u32, + key_raw: String, + new_key_raw: String, +) -> Result<(), String> { + ensure_connection_writable(&state, &connection_id, "RENAMENX").await?; + dbx_core::redis_ops::redis_rename_key_in_db_core(&state, &connection_id, db, &key_raw, &new_key_raw).await +} + #[tauri::command] pub async fn redis_hash_set( state: State<'_, Arc>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c548f14226..b171728b4d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1782,6 +1782,7 @@ pub fn run() { commands::redis_cmd::redis_get_stream_pending, commands::redis_cmd::redis_set_string, commands::redis_cmd::redis_delete_key, + commands::redis_cmd::redis_rename_key, commands::redis_cmd::redis_hash_set, commands::redis_cmd::redis_hash_del, commands::redis_cmd::redis_hash_field_set_ttl,