diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
index 03aaa03e52..98ba4fa87b 100644
--- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue
+++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
@@ -21,7 +21,6 @@ import * as api from "@/lib/api";
import type { RedisKeyInfo, RedisScanResult, RedisValue, HistoryEntry } from "@/lib/api";
import { uuid } from "@/lib/utils";
import { useConnectionStore } from "@/stores/connectionStore";
-import { useSettingsStore } from "@/stores/settingsStore";
import { buildRedisKeyTree, collectExpandedGroupIds, collectRedisGroupKeyRaws, flattenVisibleRedisKeyTree, mergeKeysIntoRedisKeyTree, redisKeyToFlatTreeRow, type RedisKeyTreeNode } from "@/lib/redisKeyTree";
import { classifyRedisCommandSafety } from "@/lib/redisCommandSafety";
import { isRedisMutatingCommand } from "@/lib/redisCommandTable";
@@ -31,11 +30,11 @@ import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle";
import { useToast } from "@/composables/useToast";
import { redisKeySearchPattern } from "@/lib/redisKeyPattern";
+import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redisKeyPattern";
const { t } = useI18n();
const { toast } = useToast();
const connectionStore = useConnectionStore();
-const settingsStore = useSettingsStore();
const editorFontFamilyStyle = useEditorFontFamilyStyle();
type RedisSearchMode = "key" | "value" | "all";
@@ -115,6 +114,7 @@ const searchPlaceholder = computed(() => {
});
const loadingEmptyText = computed(() => (isValueSearchMode.value && valueQuery.value ? t(searchMode.value === "all" ? "redis.searchingAll" : "redis.searchingValues") : t("redis.loadingKeys")));
const redisKeySeparator = computed(() => connectionStore.getConfig(props.connectionId)?.redis_key_separator ?? ":");
+const redisScanPageSize = computed(() => connectionStore.getConfig(props.connectionId)?.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT);
watch(redisKeySeparator, () => {
if (flatKeys.value.length > 0) rebuildTree(false);
});
@@ -202,14 +202,14 @@ function mergeTree(newKeys: RedisKeyInfo[]) {
}
async function fetchScanPage(): Promise {
- const pageSize = settingsStore.editorSettings.redisScanPageSize;
+ const pageSize = redisScanPageSize.value;
return isValueSearchMode.value ? await api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize, searchMode.value === "all") : await api.redisScanKeysBatch(props.connectionId, props.db, scanCursor.value, effectivePattern.value, pageSize, 1, false);
}
/// Batch-scan variant that performs multiple SCAN iterations server-side.
/// Dramatically reduces frontend↔backend roundtrips for bulk loading.
async function fetchScanBatchPage(maxIterations: number, options: { count?: number; includeTypes?: boolean } = {}): Promise {
- const pageSize = options.count ?? settingsStore.editorSettings.redisScanPageSize;
+ const pageSize = options.count ?? redisScanPageSize.value;
// Value search cannot be batched because each key requires a GET.
if (isValueSearchMode.value) {
return api.redisScanValues(props.connectionId, props.db, scanCursor.value, "*", valueQuery.value, pageSize, searchMode.value === "all");
@@ -269,19 +269,19 @@ async function streamValueSearch(requestId: number) {
}
async function fillInitialKeyBatch(requestId: number) {
- const targetCount = Math.max(1, settingsStore.editorSettings.redisScanPageSize);
- // Use server-side batching to fill the initial view quickly.
- // Each batch iteration does one SCAN round-trip; 5 iterations with
- // COUNT=1000 should return enough keys for most cases.
- const maxIter = Math.max(1, Math.ceil(targetCount / Math.max(1, settingsStore.editorSettings.redisScanPageSize)));
- const result = await fetchScanBatchPage(Math.min(maxIter, 8));
- if (requestId !== searchRequestId) return;
- appendScanResult(result);
- // If we still need more keys, do one more batch
- if (flatKeys.value.length < targetCount && hasMore.value && requestId === searchRequestId) {
- const result2 = await fetchScanBatchPage(Math.min(maxIter, 8));
+ // 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(result2);
+ appendScanResult(result);
+ totalIters += batchSize;
}
}
diff --git a/apps/desktop/src/lib/redisKeyPattern.ts b/apps/desktop/src/lib/redisKeyPattern.ts
index 91a9b85d79..32060f7f44 100644
--- a/apps/desktop/src/lib/redisKeyPattern.ts
+++ b/apps/desktop/src/lib/redisKeyPattern.ts
@@ -10,3 +10,10 @@ export function redisKeySearchPattern(value: string, fuzzy: boolean): string {
if (!pattern) return "*";
return fuzzy ? `*${escapeRedisGlobText(pattern, fuzzy)}*` : pattern;
}
+
+// Redis scan page size (COUNT parameter per SCAN round-trip) — shared defaults
+// and validation bounds used by the connection form and key browser.
+export const REDIS_SCAN_PAGE_SIZE_DEFAULT = 1000;
+export const REDIS_SCAN_PAGE_SIZE_MIN = 200;
+export const REDIS_SCAN_PAGE_SIZE_MAX = 10_000;
+export const REDIS_SCAN_PAGE_SIZE_OPTIONS = [200, 1000, 5000, 10_000] as const;
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 814c75eece..57573fee41 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -53,6 +53,7 @@ import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/s
import { inferMongoCompletionFields, type MongoCompletionField } from "@/lib/mongoCompletion";
import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/completionTreeIndex";
import { kvRootNodeLabel } from "@/lib/kvRootPresentation";
+import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redisKeyPattern";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
@@ -2792,7 +2793,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (cached) return cached;
return withCompletionInFlight(`${cacheKey}:redis-keys`, async () => {
await ensureConnected(connectionId);
- const pageSize = settingsStore.editorSettings.redisScanPageSize;
+ const pageSize = getConfig(connectionId)?.redis_scan_page_size ?? REDIS_SCAN_PAGE_SIZE_DEFAULT;
// Bounded multi-round SCAN: trade coverage for latency/memory safety.
const result = await api.redisScanKeysBatch(connectionId, Number(database), 0, "*", pageSize, 6, false);
const keys = result.keys.map((key) => key.key_display).slice(0, REDIS_COMPLETION_KEYS_MAX);
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index cb581e1b79..34eb986b73 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -331,7 +331,6 @@ export interface EditorSettings {
pageSize: number;
infiniteScroll: boolean;
infiniteScrollMaxRows: number;
- redisScanPageSize: number;
mongoViewMode: "document" | "table";
showColumnCommentsInHeader: boolean;
showColumnTypesInHeader: boolean;
@@ -435,7 +434,6 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
pageSize: 100,
infiniteScroll: false,
infiniteScrollMaxRows: 5000,
- redisScanPageSize: 1000,
mongoViewMode: "document",
showColumnCommentsInHeader: true,
showColumnTypesInHeader: true,
@@ -606,7 +604,6 @@ export function normalizeEditorSettings(settings: Partial, exist
pageSize: normalizeResultPageSize(settings.pageSize),
infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll,
infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows,
- redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize,
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
@@ -778,7 +775,6 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll;
if (partial.infiniteScrollMaxRows !== undefined)
editorSettings.value.infiniteScrollMaxRows = typeof partial.infiniteScrollMaxRows === "number" && partial.infiniteScrollMaxRows >= 1000 && partial.infiniteScrollMaxRows <= 50000 ? Math.round(partial.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
- if (partial.redisScanPageSize !== undefined) editorSettings.value.redisScanPageSize = partial.redisScanPageSize;
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;
if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 071a75105b..3cf5d76092 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -146,6 +146,7 @@ export interface ConnectionConfig {
redis_sentinel_tls?: boolean;
redis_cluster_nodes?: string;
redis_key_separator?: string;
+ redis_scan_page_size?: number;
etcd_endpoints?: string;
gbase_server?: string;
informix_server?: string;
diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs
index 2375093c7a..1de88a64ff 100644
--- a/crates/dbx-core/src/agent_connection.rs
+++ b/crates/dbx-core/src/agent_connection.rs
@@ -606,6 +606,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/cloud_sync.rs b/crates/dbx-core/src/cloud_sync.rs
index e3c28f1b75..7033d5ee83 100644
--- a/crates/dbx-core/src/cloud_sync.rs
+++ b/crates/dbx-core/src/cloud_sync.rs
@@ -639,6 +639,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index a3844088e4..0c583be45a 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -2416,6 +2416,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs
index 743ac6edb0..a2b75d33bb 100644
--- a/crates/dbx-core/src/connection_secrets.rs
+++ b/crates/dbx-core/src/connection_secrets.rs
@@ -728,6 +728,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs
index 71384038dd..b8455f086f 100644
--- a/crates/dbx-core/src/db/redis_driver.rs
+++ b/crates/dbx-core/src/db/redis_driver.rs
@@ -2615,6 +2615,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs
index b6fc4c3524..0ef642f4f5 100644
--- a/crates/dbx-core/src/models/connection.rs
+++ b/crates/dbx-core/src/models/connection.rs
@@ -67,6 +67,8 @@ pub struct ConnectionConfig {
pub redis_cluster_nodes: String,
#[serde(default = "default_redis_key_separator", skip_serializing_if = "is_default_redis_separator")]
pub redis_key_separator: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub redis_scan_page_size: Option,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub etcd_endpoints: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
@@ -397,6 +399,8 @@ struct ConnectionConfigData {
#[serde(default = "default_redis_key_separator")]
pub redis_key_separator: String,
#[serde(default)]
+ pub redis_scan_page_size: Option,
+ #[serde(default)]
pub etcd_endpoints: String,
#[serde(default)]
pub gbase_server: String,
@@ -452,6 +456,7 @@ impl From for ConnectionConfig {
redis_sentinel_tls: data.redis_sentinel_tls,
redis_cluster_nodes: data.redis_cluster_nodes,
redis_key_separator: data.redis_key_separator,
+ redis_scan_page_size: data.redis_scan_page_size,
etcd_endpoints: data.etcd_endpoints,
gbase_server: data.gbase_server,
informix_server: data.informix_server,
@@ -1582,6 +1587,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/mq/config.rs b/crates/dbx-core/src/mq/config.rs
index 1e8a45fba0..4918a67622 100644
--- a/crates/dbx-core/src/mq/config.rs
+++ b/crates/dbx-core/src/mq/config.rs
@@ -132,6 +132,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: String::new(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/mq/service.rs b/crates/dbx-core/src/mq/service.rs
index bdf588e59b..be147ec950 100644
--- a/crates/dbx-core/src/mq/service.rs
+++ b/crates/dbx-core/src/mq/service.rs
@@ -526,6 +526,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/nacos/config.rs b/crates/dbx-core/src/nacos/config.rs
index ec1886956b..76230be3e3 100644
--- a/crates/dbx-core/src/nacos/config.rs
+++ b/crates/dbx-core/src/nacos/config.rs
@@ -159,6 +159,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/nacos/service.rs b/crates/dbx-core/src/nacos/service.rs
index 938a22ec21..d79acc52cf 100644
--- a/crates/dbx-core/src/nacos/service.rs
+++ b/crates/dbx-core/src/nacos/service.rs
@@ -203,6 +203,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
@@ -266,6 +267,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs
index 83120379d3..d782530799 100644
--- a/crates/dbx-core/src/query.rs
+++ b/crates/dbx-core/src/query.rs
@@ -2468,6 +2468,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs
index 1a780560e7..122d9af67d 100644
--- a/crates/dbx-core/src/schema.rs
+++ b/crates/dbx-core/src/schema.rs
@@ -1311,6 +1311,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs
index 04cf4d7f12..03134ed01e 100644
--- a/crates/dbx-core/src/storage.rs
+++ b/crates/dbx-core/src/storage.rs
@@ -1910,6 +1910,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: ":".to_string(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs
index 235da32348..6ff619a99f 100644
--- a/crates/dbx-core/src/transfer.rs
+++ b/crates/dbx-core/src/transfer.rs
@@ -3742,6 +3742,7 @@ mod tests {
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
+ redis_scan_page_size: None,
etcd_endpoints: String::new(),
gbase_server: String::new(),
informix_server: String::new(),
diff --git a/packages/app-tests/settingsStore.test.ts b/packages/app-tests/settingsStore.test.ts
index fbe0ae1e2b..f3932c7168 100644
--- a/packages/app-tests/settingsStore.test.ts
+++ b/packages/app-tests/settingsStore.test.ts
@@ -39,15 +39,6 @@ function withMockLocalStorage(initial: Record, run: () => void)
}
}
-test("defaults Redis scan page size to 1000 keys", () => {
- assert.equal(DEFAULT_EDITOR_SETTINGS.redisScanPageSize, 1000);
- assert.equal(normalizeEditorSettings({}).redisScanPageSize, 1000);
-});
-
-test("keeps a saved Redis scan page size", () => {
- assert.equal(normalizeEditorSettings({ redisScanPageSize: 5000 }).redisScanPageSize, 5000);
-});
-
test("normalizes saved query result page size", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.pageSize, 100);
assert.equal(normalizeEditorSettings({ pageSize: 5000 }).pageSize, 5000);