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") }}
+
+
+ {{ t("redis.noKeysInScanHint") }}
+
+
+
+ {{ 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 {