From 388d59fb4ac989cfb15eee3b50ba09ab9aa96d00 Mon Sep 17 00:00:00 2001 From: hb <34821912+chenhbb@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:09:46 +0800 Subject: [PATCH] fix(redis): handle empty scan batches and exact key lookup fallback (#1992) --- .../src/components/redis/RedisKeyBrowser.vue | 49 +++++++++---------- apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + crates/dbx-core/src/db/redis_driver.rs | 30 ++++++++++++ 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue index 98ba4fa87..ce80f1c7b 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue @@ -126,6 +126,13 @@ const fetchAllProgressText = computed(() => { } return t("redis.fetchAllProgressUnknown", { loaded: flatKeys.value.length }); }); +const keyCountText = computed(() => { + if (loading.value && flatKeys.value.length === 0) return loadingEmptyText.value; + if (!isSearchMode.value && lastTotalKeys.value > 0) { + return t("redis.loadedKeys", { loaded: flatKeys.value.length, total: lastTotalKeys.value }); + } + return t("redis.keys", { count: flatKeys.value.length }); +}); const selectedKey = computed(() => flatKeys.value.find((key) => key.key_raw === selectedKeyRaw.value) ?? null); const dangerDetails = computed(() => { if (!pendingDanger.value) return ""; @@ -268,23 +275,6 @@ async function streamValueSearch(requestId: number) { } } -async function fillInitialKeyBatch(requestId: number) { - // Initial batch should fetch a substantial number of keys regardless of - // redisScanPageSize. We scan in batches of at most 8 iterations each and - // keep going until we've done at least 16 total iterations (enough to cover - // most key spaces even with COUNT=200) or hasMore becomes false. - const MAX_TOTAL_ITERATIONS = 16; - const BATCH_ITERATIONS = 8; - let totalIters = 0; - while (requestId === searchRequestId && hasMore.value && totalIters < MAX_TOTAL_ITERATIONS) { - const batchSize = Math.min(BATCH_ITERATIONS, MAX_TOTAL_ITERATIONS - totalIters); - const result = await fetchScanBatchPage(batchSize); - if (requestId !== searchRequestId) return; - appendScanResult(result); - totalIters += batchSize; - } -} - async function loadKeys() { if (!redisBrowserIsActive) return; const requestId = ++searchRequestId; @@ -297,18 +287,15 @@ async function loadKeys() { checkedKeys.value = new Set(); expandedGroupIds.value = new Set(); scanCursor.value = 0; + lastTotalKeys.value = 0; try { if (isValueSearchMode.value && !valueQuery.value) { hasMore.value = false; return; } const applied = await scanNextPage(requestId); - if (applied) { - if (isValueSearchMode.value) { - await streamValueSearch(requestId); - } else { - await fillInitialKeyBatch(requestId); - } + if (applied && isValueSearchMode.value) { + await streamValueSearch(requestId); } } finally { if (requestId === searchRequestId) { @@ -440,6 +427,7 @@ function resetLoadedKeys() { checkedKeys.value = new Set(); expandedGroupIds.value = new Set(); hasMore.value = false; + lastTotalKeys.value = 0; } async function deleteKeyRaws(keys: string[]) { @@ -986,12 +974,21 @@ defineExpose({ focusSearch }); - {{ loading && flatKeys.length === 0 ? loadingEmptyText : t("redis.keys", { count: flatKeys.length }) }} + {{ keyCountText }} -
- {{ t("redis.noKeys") }} +
+ +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index fa988badb..fbf7735ed 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1659,6 +1659,7 @@ export default { redis: { selectKey: "Select a key to view its value", noKeys: "No keys found", + noKeysInScanHint: "No keys found in current scan range", pattern: "pattern (e.g. user:*)", fuzzyPattern: "key contains...", fuzzyMatch: "Fuzzy", @@ -1669,6 +1670,7 @@ export default { searchByValue: "Value", searchByAll: "All", keys: "{count} keys", + loadedKeys: "{loaded} / {total} keys loaded", loadingKeys: "Loading keys...", searchingValues: "Searching values...", searchingAll: "Searching keys and values...", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index b183aa3b1..49ab6c200 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1321,6 +1321,7 @@ export default { redis: { selectKey: "Selecciona una clave para ver su valor", noKeys: "No se encontraron claves", + noKeysInScanHint: "No se encontraron claves en el rango de escaneo actual", pattern: "patrón (p. ej. usuario:*)", fuzzyPattern: "la clave contiene...", fuzzyMatch: "Difusa", @@ -1331,6 +1332,7 @@ export default { searchByValue: "Valor", searchByAll: "Todo", keys: "{count} claves", + loadedKeys: "{loaded} / {total} claves cargadas", loadingKeys: "Cargando claves...", searchingValues: "Buscando por valor...", searchingAll: "Buscando claves y valores...", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index ba36720da..9743abe55 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1445,6 +1445,7 @@ export default { redis: { selectKey: "Seleziona una chiave per visualizzarne il valore", noKeys: "Nessuna chiave trovata", + noKeysInScanHint: "Nessuna chiave trovata nell'intervallo di scansione corrente", pattern: "pattern (es. user:*)", fuzzyPattern: "la chiave contiene...", fuzzyMatch: "Fuzzy", @@ -1455,6 +1456,7 @@ export default { searchByValue: "Valore", searchByAll: "Tutto", keys: "{count} chiavi", + loadedKeys: "{loaded} / {total} chiavi caricate", loadingKeys: "Caricamento chiavi...", searchingValues: "Ricerca nei valori...", searchingAll: "Ricerca in chiavi e valori...", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b8616e9a5..c332e8b36 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1603,9 +1603,11 @@ export default { searchByValue: "値", searchByAll: "すべて", keys: "{count}キー", + loadedKeys: "{loaded}/{total}キー読み込み完了", loadingKeys: "キーを読み込み中...", searchingValues: "値を検索中...", searchingAll: "キーと値を検索中...", + noKeysInScanHint: "現在のスキャン範囲で一致するキーは見つかりません", loadMoreKeys: "さらにキーを読み込む", fetchAllKeys: "すべて取得", stopFetchAll: "停止", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 9e409494a..c78a26ecd 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1456,6 +1456,7 @@ export default { redis: { selectKey: "Selecione uma chave para ver seu valor", noKeys: "Nenhuma chave encontrada", + noKeysInScanHint: "Nenhuma chave encontrada no intervalo atual de verificação", pattern: "padrão (ex.: user:*)", fuzzyPattern: "a chave contém...", fuzzyMatch: "Aproximado", @@ -1466,6 +1467,7 @@ export default { searchByValue: "Valor", searchByAll: "Tudo", keys: "{count} chaves", + loadedKeys: "{loaded} / {total} chaves carregadas", loadingKeys: "Carregando chaves...", searchingValues: "Pesquisando valores...", searchingAll: "Pesquisando chaves e valores...", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 57905bee3..6aeda3fa9 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1658,6 +1658,7 @@ export default { redis: { selectKey: "选择一个 key 查看值", noKeys: "未找到 key", + noKeysInScanHint: "当前扫描范围内未命中匹配的 key", pattern: "匹配模式 (如 user:*)", fuzzyPattern: "输入关键字搜索 key", fuzzyMatch: "模糊", @@ -1668,6 +1669,7 @@ export default { searchByValue: "值", searchByAll: "全部", keys: "{count} 个 key", + loadedKeys: "已加载 {loaded} / 共 {total} 个 key", loadingKeys: "正在加载 key...", searchingValues: "正在按值搜索...", searchingAll: "正在搜索 key 和值...", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 03f1b15c4..d96432511 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1436,6 +1436,7 @@ export default { redis: { selectKey: "選擇一個 key 檢視值", noKeys: "未找到 key", + noKeysInScanHint: "目前掃描範圍內未找到匹配的 key", pattern: "模式 (例如 user:*)", fuzzyPattern: "輸入關鍵字搜尋 key", fuzzyMatch: "模糊", @@ -1446,6 +1447,7 @@ export default { searchByValue: "值", searchByAll: "全部", keys: "{count} 個 key", + loadedKeys: "已載入 {loaded} / 共 {total} 個 key", loadingKeys: "正在載入 key……", searchingValues: "正在按值搜尋……", searchingAll: "正在搜尋鍵和值……", diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs index b8455f086..371d9438b 100644 --- a/crates/dbx-core/src/db/redis_driver.rs +++ b/crates/dbx-core/src/db/redis_driver.rs @@ -1495,7 +1495,37 @@ where let iterations = max_iterations.max(1); let total_keys: u64 = if cursor == 0 { redis::cmd("DBSIZE").query_async(con).await.unwrap_or(0) } else { 0 }; + let is_exact_match = !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('['); + if cursor == 0 && is_exact_match && !pattern.is_empty() { + match redis::cmd("EXISTS").arg(pattern).query_async::(con).await { + Ok(true) => { + let key_type: String = if include_types { + redis::cmd("TYPE").arg(pattern).query_async(con).await.unwrap_or_else(|_| "unknown".to_string()) + } else { + String::new() + }; + + let value_preview = if include_types { redis_key_value_preview(&key_type) } else { String::new() }; + + let key_info = RedisKeyInfo { + key_display: redis_key_bytes_to_display(pattern.as_bytes()), + key_raw: redis_key_bytes_to_raw(pattern.as_bytes()), + key_type, + ttl: -2, + size: 0, + value_preview, + }; + return Ok(RedisScanResult { cursor: 0, keys: vec![key_info], total_keys }); + } + Ok(false) => { + return Ok(RedisScanResult { cursor: 0, keys: vec![], total_keys }); + } + Err(_) => {} + } + } + let mut all_keys: Vec = Vec::new(); + let mut current_cursor = cursor; for _ in 0..iterations {