diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index 30daf2e9fb..a7de81f529 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -119,6 +119,7 @@ const defaultForm = (): ConnectionForm => ({
redis_sentinel_password: "",
redis_sentinel_tls: false,
redis_cluster_nodes: "",
+ redis_key_separator: ":",
etcd_endpoints: "",
read_only: false,
visible_databases: undefined,
@@ -545,6 +546,7 @@ watch(
redis_sentinel_password: config.redis_sentinel_password || "",
redis_sentinel_tls: config.redis_sentinel_tls || false,
redis_cluster_nodes: config.redis_cluster_nodes || "",
+ redis_key_separator: config.redis_key_separator || ":",
etcd_endpoints: config.etcd_endpoints || "",
read_only: config.read_only || false,
visible_databases: config.visible_databases,
@@ -1057,6 +1059,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.redis_sentinel_password = undefined;
config.redis_sentinel_tls = undefined;
config.redis_cluster_nodes = undefined;
+ config.redis_key_separator = undefined;
} else if (config.redis_connection_mode === "sentinel") {
config.redis_sentinel_master = config.redis_sentinel_master?.trim() || "";
config.redis_sentinel_nodes = normalizeRedisSentinelNodes(config.redis_sentinel_nodes || "");
@@ -1088,6 +1091,9 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
config.redis_sentinel_tls = undefined;
config.redis_cluster_nodes = undefined;
}
+ if (config.db_type === "redis") {
+ config.redis_key_separator = config.redis_key_separator?.trim() || ":";
+ }
if (config.db_type === "etcd") {
config.etcd_endpoints = normalizeEndpointLines(config.etcd_endpoints || "");
const firstEndpoint = firstEtcdEndpoint(config.etcd_endpoints);
@@ -2376,6 +2382,10 @@ function openExternalUrl(url: string) {
+
+
+
+
diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
index 578e57a5d4..20fcea0705 100644
--- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue
+++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue
@@ -104,6 +104,7 @@ const effectivePattern = computed(() => (searchMode.value === "key" ? redisKeySe
const isSearchMode = computed(() => (searchMode.value === "key" ? effectivePattern.value !== "*" : valueQuery.value !== ""));
const searchPlaceholder = computed(() => (searchMode.value === "key" ? (fuzzyKeySearch.value ? t("redis.fuzzyPattern") : t("redis.pattern")) : t("redis.valueSearchPlaceholder")));
const loadingEmptyText = computed(() => (searchMode.value === "value" && valueQuery.value ? t("redis.searchingValues") : t("redis.loadingKeys")));
+const redisKeySeparator = computed(() => connectionStore.getConfig(props.connectionId)?.redis_key_separator || ":");
const lastTotalKeys = ref(0);
const fetchAllProgressText = computed(() => {
if (!isFetchingAll.value) return "";
@@ -151,7 +152,7 @@ function countLeaves(node: RedisKeyTreeNode): number {
}
function rebuildTree(expandAll = false) {
- const nextTree = buildRedisKeyTree(flatKeys.value, props.db);
+ const nextTree = buildRedisKeyTree(flatKeys.value, props.db, redisKeySeparator.value);
treeKeys.value = nextTree;
const nextExpanded = new Set();
@@ -172,7 +173,7 @@ function rebuildTree(expandAll = false) {
function mergeTree(newKeys: RedisKeyInfo[]) {
if (newKeys.length === 0) return;
- treeKeys.value = mergeKeysIntoRedisKeyTree(treeKeys.value, newKeys, props.db);
+ treeKeys.value = mergeKeysIntoRedisKeyTree(treeKeys.value, newKeys, props.db, redisKeySeparator.value);
const availableExpanded = collectExpandedGroupIds(treeKeys.value);
const nextExpanded = new Set();
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index e0fa0d8d18..259747b07f 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -222,6 +222,7 @@
redisSentinelPassword: "Sentinel Password",
redisSentinelTls: "Sentinel TLS",
redisSentinelTlsHint: "Use TLS when connecting to Sentinel nodes",
+ redisKeySeparator: "Key Namespace Separator",
etcdEndpoints: "Endpoints",
etcdEndpointsHint: "One endpoint per line. Leave blank to use the host and port above.",
etcdCaCertPlaceholder: "/path/to/ca.crt",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 716ad6c10e..39af900392 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -215,6 +215,7 @@
redisSentinelPassword: "Contraseña Sentinel",
redisSentinelTls: "TLS Sentinel",
redisSentinelTlsHint: "Usar TLS al conectar con nodos Sentinel",
+ redisKeySeparator: "Separador de espacio de nombres de clave",
searchDatabasePlaceholder: "Buscar tipos de bases de datos",
iconView: "Vista de íconos",
listView: "Vista de lista",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 7a9929714b..108cfac69f 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -220,6 +220,7 @@
redisSentinelPassword: "Password Sentinel",
redisSentinelTls: "TLS Sentinel",
redisSentinelTlsHint: "Usa TLS quando ti connetti ai nodi Sentinel",
+ redisKeySeparator: "Separatore namespace chiavi",
searchDatabasePlaceholder: "Cerca tipi di database",
iconView: "Vista icone",
listView: "Vista elenco",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 75dc933e70..db5f2c99d6 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -220,6 +220,7 @@
redisSentinelPassword: "Senha do Sentinel",
redisSentinelTls: "TLS do Sentinel",
redisSentinelTlsHint: "Usar TLS ao conectar aos nós Sentinel",
+ redisKeySeparator: "Separador de namespace de chave",
searchDatabasePlaceholder: "Pesquisar tipos de banco de dados",
iconView: "Visualização em ícones",
listView: "Visualização em lista",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 2484fa068f..96ee1ca863 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -223,6 +223,7 @@
redisSentinelPassword: "哨兵密码",
redisSentinelTls: "哨兵 TLS",
redisSentinelTlsHint: "连接 Sentinel 节点时使用 TLS",
+ redisKeySeparator: "键名命名空间分隔符",
etcdEndpoints: "Endpoints",
etcdEndpointsHint: "每行一个 endpoint。留空时使用上面的 host 和端口。",
etcdCaCertPlaceholder: "/path/to/ca.crt",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index af9c5882ec..648505702d 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -220,6 +220,7 @@
redisSentinelPassword: "Sentinel 密碼",
redisSentinelTls: "Sentinel TLS",
redisSentinelTlsHint: "連線 Sentinel 節點時使用 TLS",
+ redisKeySeparator: "鍵名命名空間分隔符",
searchDatabasePlaceholder: "搜尋資料庫類型",
iconView: "圖示檢視",
listView: "清單檢視",
diff --git a/apps/desktop/src/lib/redisKeyTree.ts b/apps/desktop/src/lib/redisKeyTree.ts
index fb3892e4db..3eb3ec8017 100644
--- a/apps/desktop/src/lib/redisKeyTree.ts
+++ b/apps/desktop/src/lib/redisKeyTree.ts
@@ -53,19 +53,19 @@ function sortRedisTreeNodes(nodes: RedisKeyTreeNode[]): RedisKeyTreeNode[] {
);
}
-export function buildRedisKeyTree(keys: RedisKeyInfo[], db: number): RedisKeyTreeNode[] {
+export function buildRedisKeyTree(keys: RedisKeyInfo[], db: number, separator = ":"): RedisKeyTreeNode[] {
const root: RedisKeyTreeNode[] = [];
const groupMap = new Map();
for (const key of keys) {
- insertKeyIntoTree(root, groupMap, key, db);
+ insertKeyIntoTree(root, groupMap, key, db, separator);
}
return sortRedisTreeNodes(root);
}
-function insertKeyIntoTree(root: RedisKeyTreeNode[], groupMap: Map, key: RedisKeyInfo, db: number): void {
- const pathSegments = key.key_display.split(":");
+function insertKeyIntoTree(root: RedisKeyTreeNode[], groupMap: Map, key: RedisKeyInfo, db: number, separator: string): void {
+ const pathSegments = key.key_display.split(separator);
if (pathSegments.length === 1) {
root.push({
kind: "leaf",
@@ -134,8 +134,8 @@ function rebuildGroupMap(tree: RedisKeyTreeNode[]): Map();
@@ -152,7 +152,7 @@ export function mergeKeysIntoRedisKeyTree(existingTree: RedisKeyTreeNode[], newK
for (const key of newKeys) {
if (existingKeyIds.has(key.key_raw)) continue;
- insertKeyIntoTree(existingTree, groupMap, key, db);
+ insertKeyIntoTree(existingTree, groupMap, key, db, separator);
}
return sortRedisTreeNodes(existingTree);
diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs
index 8b12743fe9..ed191afd4b 100644
--- a/crates/dbx-core/src/agent_connection.rs
+++ b/crates/dbx-core/src/agent_connection.rs
@@ -1,6 +1,6 @@
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
-use crate::models::connection::{ConnectionConfig, DatabaseType};
+use crate::models::connection::{default_redis_key_separator, ConnectionConfig, DatabaseType};
use crate::path_utils::expand_tilde;
pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> serde_json::Value {
@@ -428,6 +428,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/cloud_sync.rs b/crates/dbx-core/src/cloud_sync.rs
index b624b733a2..742f655a73 100644
--- a/crates/dbx-core/src/cloud_sync.rs
+++ b/crates/dbx-core/src/cloud_sync.rs
@@ -530,7 +530,9 @@ mod tests {
decrypt_sensitive_payload, encrypt_sensitive_payload, normalized_remote_path, parent_collection_paths,
scrub_connection_secrets, ConnectionSecretSnapshot, SensitiveSyncPayload,
};
- use crate::models::connection::{ConnectionConfig, DatabaseType, TransportLayerConfig};
+ use crate::models::connection::{
+ default_redis_key_separator, ConnectionConfig, DatabaseType, TransportLayerConfig,
+ };
#[test]
fn normalizes_empty_remote_path_to_default() {
@@ -591,6 +593,7 @@ mod tests {
redis_sentinel_password: "sentinel".to_string(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index 8037a4dd93..fd755df22d 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -19,7 +19,8 @@ use crate::db::proxy_tunnel::ProxyTunnelManager;
use crate::db::ssh_tunnel::TunnelManager;
use crate::external;
use crate::models::connection::{
- parse_jdbc_host_port, parse_mongo_first_host, rewrite_jdbc_url_host, ConnectionConfig, DatabaseType,
+ default_redis_key_separator, parse_jdbc_host_port, parse_mongo_first_host, rewrite_jdbc_url_host, ConnectionConfig,
+ DatabaseType,
};
use crate::path_utils::expand_tilde;
use crate::plugins::{PluginDriverSession, PluginRegistry, PluginRuntimeEnv};
@@ -1205,8 +1206,8 @@ mod tests {
use crate::agent_manager::{AgentState, JavaRuntimeConfig, JavaRuntimeMode, DEFAULT_JRE_KEY};
use crate::db;
use crate::models::connection::{
- default_connect_timeout_secs, ConnectionConfig, DatabaseType, ProxyTunnelConfig, ProxyType,
- TransportLayerConfig,
+ default_connect_timeout_secs, default_redis_key_separator, ConnectionConfig, DatabaseType, ProxyTunnelConfig,
+ ProxyType, TransportLayerConfig,
};
use crate::query;
use crate::schema;
@@ -1246,6 +1247,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs
index a807907b42..a140a202da 100644
--- a/crates/dbx-core/src/connection_secrets.rs
+++ b/crates/dbx-core/src/connection_secrets.rs
@@ -506,6 +506,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: crate::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs
index 37bd4f726e..60302c9023 100644
--- a/crates/dbx-core/src/models/connection.rs
+++ b/crates/dbx-core/src/models/connection.rs
@@ -60,6 +60,8 @@ pub struct ConnectionConfig {
pub redis_sentinel_tls: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
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 = "String::is_empty")]
pub etcd_endpoints: String,
/// Typed configuration for external tabular sources.
@@ -196,6 +198,14 @@ fn is_false(value: &bool) -> bool {
!*value
}
+pub fn default_redis_key_separator() -> String {
+ ":".to_string()
+}
+
+fn is_default_redis_separator(value: &str) -> bool {
+ value == ":"
+}
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
@@ -337,6 +347,8 @@ struct ConnectionConfigData {
pub redis_sentinel_tls: bool,
#[serde(default)]
pub redis_cluster_nodes: String,
+ #[serde(default = "default_redis_key_separator")]
+ pub redis_key_separator: String,
#[serde(default)]
pub etcd_endpoints: String,
#[serde(default)]
@@ -386,6 +398,7 @@ impl From for ConnectionConfig {
redis_sentinel_password: data.redis_sentinel_password,
redis_sentinel_tls: data.redis_sentinel_tls,
redis_cluster_nodes: data.redis_cluster_nodes,
+ redis_key_separator: data.redis_key_separator,
etcd_endpoints: data.etcd_endpoints,
external_config: data.external_config,
jdbc_driver_class: data.jdbc_driver_class,
@@ -1337,8 +1350,8 @@ fn bracket_ipv6(host: &str) -> String {
#[cfg(test)]
mod tests {
use super::{
- default_query_timeout_secs, default_ssh_connect_timeout_secs, ConnectionConfig, DatabaseType,
- ProxyTunnelConfig, ProxyType, TransportLayerConfig,
+ default_query_timeout_secs, default_redis_key_separator, default_ssh_connect_timeout_secs, ConnectionConfig,
+ DatabaseType, ProxyTunnelConfig, ProxyType, TransportLayerConfig,
};
use std::str::FromStr;
@@ -1376,6 +1389,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs
index bed6b92646..9117b51a15 100644
--- a/crates/dbx-core/src/query.rs
+++ b/crates/dbx-core/src/query.rs
@@ -8,7 +8,7 @@ use tokio_util::sync::CancellationToken;
use crate::connection::{AppState, PoolKind};
use crate::db;
-use crate::models::connection::DatabaseType;
+use crate::models::connection::{default_redis_key_separator, DatabaseType};
use crate::sql::{split_sql_batches, split_sql_statements, starts_with_duckdb_result_sql_keyword};
pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
@@ -1848,6 +1848,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs
index d5a670b91c..4345a8529e 100644
--- a/crates/dbx-core/src/schema.rs
+++ b/crates/dbx-core/src/schema.rs
@@ -1,6 +1,6 @@
use crate::connection::{connection_url_for_endpoint, database_connection_config, AppState, MysqlMode, PoolKind};
use crate::db;
-use crate::models::connection::{ConnectionConfig, DatabaseType};
+use crate::models::connection::{default_redis_key_separator, ConnectionConfig, DatabaseType};
use crate::query::{agent_execute_query_params, QueryExecutionOptions};
use std::collections::HashSet;
use std::future::Future;
@@ -614,7 +614,7 @@ mod tests {
duckdb_query_tables_in_database, filter_objects_by_types, filter_yashandb_recyclebin_objects,
filter_yashandb_recyclebin_tables, is_agent_postgres_metadata_fallback_config,
};
- use crate::models::connection::{ConnectionConfig, DatabaseType};
+ use crate::models::connection::{default_redis_key_separator, ConnectionConfig, DatabaseType};
fn test_column(name: &str, comment: Option<&str>, is_primary_key: bool) -> super::db::ColumnInfo {
super::db::ColumnInfo {
@@ -665,6 +665,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs
index 37722708aa..6ab4cec83a 100644
--- a/crates/dbx-core/src/transfer.rs
+++ b/crates/dbx-core/src/transfer.rs
@@ -6,7 +6,7 @@ use tokio::sync::RwLock;
use crate::connection::{AppState, PoolKind};
use crate::db;
use crate::db::mongo_driver::MongoDocumentResult;
-use crate::models::connection::DatabaseType;
+use crate::models::connection::{default_redis_key_separator, DatabaseType};
use crate::object_source_sql::{build_executable_object_source_statements, EditableObjectSourceSqlInput};
use crate::query::{agent_execute_query_params, QueryExecutionOptions};
use crate::sql::starts_with_executable_sql_keyword;
@@ -3161,6 +3161,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-core/tests/live_postgres_transfer.rs b/crates/dbx-core/tests/live_postgres_transfer.rs
index 26d1cbdb8b..0dfd4208d9 100644
--- a/crates/dbx-core/tests/live_postgres_transfer.rs
+++ b/crates/dbx-core/tests/live_postgres_transfer.rs
@@ -42,6 +42,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs
index ee9bd0162d..aa998a0e24 100644
--- a/crates/dbx-web/src/routes/connection.rs
+++ b/crates/dbx-web/src/routes/connection.rs
@@ -197,6 +197,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
+ redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
external_config: None,
jdbc_driver_class: None,
diff --git a/packages/node-core/src/connections.ts b/packages/node-core/src/connections.ts
index 7d645a2d3f..1ce9587ce9 100644
--- a/packages/node-core/src/connections.ts
+++ b/packages/node-core/src/connections.ts
@@ -25,6 +25,7 @@ export interface ConnectionConfig {
redis_sentinel_username?: string;
redis_sentinel_password?: string;
redis_sentinel_tls?: boolean;
+ redis_key_separator?: string;
read_only?: boolean;
}