diff --git a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue index 5b29916b6..2b79aa777 100644 --- a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue +++ b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue @@ -5,9 +5,12 @@ import { CheckSquare, Loader2, Search, Square } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { useDatabaseOptions } from "@/composables/useDatabaseOptions"; import { useConnectionStore } from "@/stores/connectionStore"; -import { normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { + filterDatabaseNamesForConnection, + isSystemDatabaseName, + normalizeVisibleDatabaseSelection, +} from "@/lib/visibleDatabases"; import * as api from "@/lib/api"; const props = defineProps<{ @@ -22,22 +25,29 @@ const emit = defineEmits<{ const { t } = useI18n(); const connectionStore = useConnectionStore(); -const { getDatabaseOptions } = useDatabaseOptions(); const databaseNames = ref([]); const selectedNames = ref>(new Set()); const searchText = ref(""); +const showSystemDatabases = ref(false); const isLoading = ref(false); const errorMessage = ref(""); const connection = computed(() => connectionStore.getConfig(props.connectionId)); +const listedDatabaseNames = computed(() => { + if (showSystemDatabases.value) return databaseNames.value; + return filterDatabaseNamesForConnection(databaseNames.value, connection.value); +}); const filteredDatabaseNames = computed(() => { const query = searchText.value.trim().toLowerCase(); - if (!query) return databaseNames.value; - return databaseNames.value.filter((name) => name.toLowerCase().includes(query)); + if (!query) return listedDatabaseNames.value; + return listedDatabaseNames.value.filter((name) => name.toLowerCase().includes(query)); }); const selectedCount = computed(() => selectedNames.value.size); -const totalCount = computed(() => databaseNames.value.length); +const totalCount = computed(() => listedDatabaseNames.value.length); +const hasSystemDatabases = computed(() => + databaseNames.value.some((database) => isSystemDatabaseName(connection.value?.db_type, database)), +); watch( () => props.open, @@ -47,6 +57,13 @@ watch( }, ); +watch(showSystemDatabases, (show) => { + if (show) return; + selectedNames.value = new Set( + [...selectedNames.value].filter((database) => !isSystemDatabaseName(connection.value?.db_type, database)), + ); +}); + async function loadDatabases() { isLoading.value = true; errorMessage.value = ""; @@ -55,11 +72,17 @@ async function loadDatabases() { const names = await loadDatabaseNames(); databaseNames.value = names; const configured = connection.value?.visible_databases; - const initialSelection = Array.isArray(configured) ? normalizeVisibleDatabaseSelection(configured, names) : names; + const initialSelection = Array.isArray(configured) + ? normalizeVisibleDatabaseSelection(configured, names) + : filterDatabaseNamesForConnection(names, connection.value); selectedNames.value = new Set(initialSelection); + showSystemDatabases.value = initialSelection.some((database) => + isSystemDatabaseName(connection.value?.db_type, database), + ); } catch (e: any) { databaseNames.value = []; selectedNames.value = new Set(); + showSystemDatabases.value = false; errorMessage.value = String(e?.message || e); } finally { isLoading.value = false; @@ -72,7 +95,14 @@ async function loadDatabaseNames(): Promise { await connectionStore.ensureConnected(props.connectionId); return api.listSchemas(props.connectionId, config.database || ""); } - return getDatabaseOptions(props.connectionId); + await connectionStore.ensureConnected(props.connectionId); + if (config?.db_type === "redis") { + return (await api.redisListDatabases(props.connectionId)).map((database) => String(database.db)); + } + if (config?.db_type === "mongodb") { + return api.mongoListDatabases(props.connectionId); + } + return (await api.listDatabases(props.connectionId)).map((database) => database.name); } function toggleDatabase(database: string) { @@ -83,7 +113,7 @@ function toggleDatabase(database: string) { } function selectAll() { - selectedNames.value = new Set(databaseNames.value); + selectedNames.value = new Set(listedDatabaseNames.value); } function clearSelection() { @@ -140,6 +170,19 @@ async function saveSelection() { + +
diff --git a/apps/desktop/src/composables/useDatabaseOptions.ts b/apps/desktop/src/composables/useDatabaseOptions.ts index b0bfec111..ab49acc33 100644 --- a/apps/desktop/src/composables/useDatabaseOptions.ts +++ b/apps/desktop/src/composables/useDatabaseOptions.ts @@ -1,5 +1,6 @@ import { ref } from "vue"; import { useConnectionStore } from "@/stores/connectionStore"; +import { filterDatabaseNamesForConnection } from "@/lib/visibleDatabases"; import * as api from "@/lib/api"; export function useDatabaseOptions() { @@ -19,10 +20,16 @@ export function useDatabaseOptions() { const dbs = await api.redisListDatabases(connectionId); databaseOptions.value[connectionId] = dbs.map((db) => String(db.db)); } else if (connection.db_type === "mongodb") { - databaseOptions.value[connectionId] = await api.mongoListDatabases(connectionId); + databaseOptions.value[connectionId] = filterDatabaseNamesForConnection( + await api.mongoListDatabases(connectionId), + connection, + ); } else { const dbs = await api.listDatabases(connectionId); - databaseOptions.value[connectionId] = dbs.map((db) => db.name); + databaseOptions.value[connectionId] = filterDatabaseNamesForConnection( + dbs.map((db) => db.name), + connection, + ); } } finally { loadingDatabaseOptions.value[connectionId] = false; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index bcaa82355..c4d7ccd9e 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -854,6 +854,7 @@ export default { selectAll: "Select all", clear: "Clear", showAll: "Show all", + showSystemDatabases: "Show system databases", save: "Save", loadFailed: "Failed to load databases: {message}", }, diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 018eb7ae1..30bc29b9c 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -747,6 +747,7 @@ export default { selectAll: "Seleccionar todo", clear: "Limpiar", showAll: "Mostrar todo", + showSystemDatabases: "Mostrar bases de datos del sistema", save: "Guardar", loadFailed: "No se pudieron cargar las bases de datos: {message}", }, diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 3b17ffbbb..d8dd42ef1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -835,6 +835,7 @@ export default { selectAll: "全选", clear: "清空", showAll: "显示全部", + showSystemDatabases: "显示系统库", save: "保存", loadFailed: "加载数据库失败:{message}", }, diff --git a/apps/desktop/src/lib/visibleDatabases.ts b/apps/desktop/src/lib/visibleDatabases.ts index 7cd6a3063..8637182a9 100644 --- a/apps/desktop/src/lib/visibleDatabases.ts +++ b/apps/desktop/src/lib/visibleDatabases.ts @@ -1,3 +1,68 @@ +import type { ConnectionConfig, DatabaseType } from "@/types/database"; + +const SYSTEM_DATABASE_RULES: Partial>> = { + mysql: new Set(["information_schema", "mysql", "performance_schema", "sys"]), + doris: new Set(["information_schema", "mysql", "performance_schema", "sys"]), + starrocks: new Set(["information_schema", "mysql", "performance_schema", "sys"]), + goldendb: new Set(["information_schema", "mysql", "performance_schema", "sys"]), + gbase: new Set(["information_schema", "mysql", "performance_schema", "sys"]), + postgres: new Set(["template0", "template1"]), + gaussdb: new Set(["template0", "template1"]), + opengauss: new Set(["template0", "template1"]), + kingbase: new Set(["template0", "template1"]), + highgo: new Set(["template0", "template1"]), + vastbase: new Set(["template0", "template1"]), + redshift: new Set(["template0", "template1"]), + clickhouse: new Set(["information_schema", "system"]), + sqlserver: new Set(["master", "model", "msdb", "tempdb"]), + mongodb: new Set(["admin", "config", "local"]), + oracle: new Set([ + "anonymous", + "appqossys", + "audsys", + "ctxsys", + "dbsnmp", + "dip", + "dvf", + "dvsys", + "exfsys", + "flows_files", + "gsmadmin_internal", + "mddata", + "mdsys", + "mgmt_view", + "olapsys", + "orddata", + "ordplugins", + "ordsys", + "outln", + "owbsys", + "remote_scheduler_agent", + "si_informtn_schema", + "sys", + "sysback", + "sysdg", + "syskm", + "system", + "wmsys", + "xdb", + "xs$null", + ]), + dameng: new Set(["ctisys", "dba", "sys", "sysauditor", "sysdba", "syssso", "system"]), + saphana: new Set(["_sys_afl", "_sys_bi", "_sys_bic", "_sys_repo", "_sys_statistics", "sys"]), + cassandra: new Set([ + "system", + "system_auth", + "system_distributed", + "system_schema", + "system_traces", + "system_views", + "system_virtual_schema", + ]), + neo4j: new Set(["system"]), + snowflake: new Set(["snowflake", "snowflake_sample_data"]), +}; + export function visibleDatabaseFilterIsEnabled(visibleDatabases: string[] | undefined): boolean { return Array.isArray(visibleDatabases); } @@ -17,3 +82,19 @@ export function normalizeVisibleDatabaseSelection(selectedNames: string[], datab return true; }); } + +export function isSystemDatabaseName(databaseType: DatabaseType | undefined, databaseName: string): boolean { + if (!databaseType) return false; + return SYSTEM_DATABASE_RULES[databaseType]?.has(databaseName.toLowerCase()) ?? false; +} + +export function filterDatabaseNamesForConnection( + databaseNames: string[], + connection: Pick | undefined, +): string[] { + const visibleDatabases = connection?.visible_databases; + if (visibleDatabaseFilterIsEnabled(visibleDatabases)) { + return filterVisibleDatabaseNames(databaseNames, visibleDatabases); + } + return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name)); +} diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 33b634fce..a70759954 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -25,7 +25,11 @@ import { buildDatabaseTreeNodes } from "@/lib/databaseTree"; import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree"; import { findDatabaseTreeNode } from "@/lib/treeRefreshTarget"; import { shouldMarkDisconnected } from "@/lib/connectionHealth"; -import { filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { + filterDatabaseNamesForConnection, + filterVisibleDatabaseNames, + normalizeVisibleDatabaseSelection, +} from "@/lib/visibleDatabases"; import { buildGroupedObjectTreeNodes, buildTableTreeNodes, @@ -695,7 +699,7 @@ export const useConnectionStore = defineStore("connection", () => { } } const schemas = await api.listSchemas(connectionId, effectiveDb); - const visibleSchemas = filterVisibleDatabaseNames(schemas, config?.visible_databases); + const visibleSchemas = filterDatabaseNamesForConnection(schemas, config); const schemaNodes: TreeNode[] = visibleSchemas.map((s) => ({ id: `${connectionId}:${s}:${s}`, label: s, @@ -718,9 +722,9 @@ export const useConnectionStore = defineStore("connection", () => { } } const databases = await api.listDatabases(connectionId); - const visibleNames = filterVisibleDatabaseNames( + const visibleNames = filterDatabaseNamesForConnection( databases.map((database) => database.name), - config?.visible_databases, + config, ); const visibleNameSet = new Set(visibleNames); const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name)); @@ -810,7 +814,7 @@ export const useConnectionStore = defineStore("connection", () => { await ensureConnected(connectionId); const dbs = await api.mongoListDatabases(connectionId); const config = getConfig(connectionId); - const visibleDbs = filterVisibleDatabaseNames(dbs, config?.visible_databases); + const visibleDbs = filterDatabaseNamesForConnection(dbs, config); setChildren( node, withSavedSqlRoot( diff --git a/crates/dbx-core/src/db/clickhouse_driver.rs b/crates/dbx-core/src/db/clickhouse_driver.rs index 68023a0da..8c557838a 100644 --- a/crates/dbx-core/src/db/clickhouse_driver.rs +++ b/crates/dbx-core/src/db/clickhouse_driver.rs @@ -172,14 +172,7 @@ pub async fn test_connection(client: &ChClient) -> Result<(), String> { } pub async fn list_databases(client: &ChClient) -> Result, String> { - let result = ch_query( - client, - "SELECT name FROM system.databases \ - WHERE name NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema') \ - ORDER BY name", - None, - ) - .await?; + let result = ch_query(client, "SELECT name FROM system.databases ORDER BY name", None).await?; Ok(result.data.iter().map(|row| DatabaseInfo { name: row[0].as_str().unwrap_or("").to_string() }).collect()) } diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 083351b57..d17fdf29c 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -274,11 +274,7 @@ pub async fn connect_bare(url: &str) -> Result { pub async fn list_databases(pool: &MySqlPool) -> Result, String> { let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; let result = conn - .query_iter( - "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA \ - WHERE SCHEMA_NAME NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') \ - ORDER BY SCHEMA_NAME", - ) + .query_iter("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA ORDER BY SCHEMA_NAME") .await .map_err(|e| e.to_string())?; let rows: Vec = result.collect_and_drop().await.map_err(|e| e.to_string())?; diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index bc668a11e..29436562f 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -261,8 +261,7 @@ pub async fn list_databases(client: &mut SqlServerClient) -> Result 4 \ - AND state = 0 \ + WHERE state = 0 \ ORDER BY name", &[], ) diff --git a/packages/app-tests/visibleDatabases.test.ts b/packages/app-tests/visibleDatabases.test.ts index ca3aa903d..7cddddade 100644 --- a/packages/app-tests/visibleDatabases.test.ts +++ b/packages/app-tests/visibleDatabases.test.ts @@ -1,7 +1,9 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { + filterDatabaseNamesForConnection, filterVisibleDatabaseNames, + isSystemDatabaseName, normalizeVisibleDatabaseSelection, visibleDatabaseFilterIsEnabled, } from "../../apps/desktop/src/lib/visibleDatabases.ts"; @@ -12,10 +14,7 @@ test("undefined visible database filter keeps every database", () => { }); test("configured visible database filter keeps selected databases in source order", () => { - assert.deepEqual(filterVisibleDatabaseNames(["app", "analytics", "billing"], ["billing", "app"]), [ - "app", - "billing", - ]); + assert.deepEqual(filterVisibleDatabaseNames(["app", "analytics", "billing"], ["billing", "app"]), ["app", "billing"]); assert.equal(visibleDatabaseFilterIsEnabled(["billing", "app"]), true); }); @@ -30,3 +29,33 @@ test("normalizes selected database names against fresh database names", () => { "app", ]); }); + +test("mysql system databases are hidden by default but can be explicitly selected", () => { + const databases = ["app", "information_schema", "mysql", "performance_schema", "sys"]; + assert.deepEqual(filterDatabaseNamesForConnection(databases, { db_type: "mysql" }), ["app"]); + assert.deepEqual( + filterDatabaseNamesForConnection(databases, { db_type: "mysql", visible_databases: ["app", "sys"] }), + ["app", "sys"], + ); + assert.equal(isSystemDatabaseName("mysql", "performance_schema"), true); + assert.equal(isSystemDatabaseName("postgres", "information_schema"), false); +}); + +test("system database detection is registered per database type", () => { + assert.deepEqual(filterDatabaseNamesForConnection(["default", "system"], { db_type: "clickhouse" }), ["default"]); + assert.deepEqual(filterDatabaseNamesForConnection(["master", "app", "tempdb"], { db_type: "sqlserver" }), ["app"]); + assert.equal(isSystemDatabaseName("clickhouse", "INFORMATION_SCHEMA"), true); + assert.equal(isSystemDatabaseName("sqlserver", "msdb"), true); +}); + +test("system database registry covers common database families", () => { + assert.deepEqual(filterDatabaseNamesForConnection(["template0", "app"], { db_type: "postgres" }), ["app"]); + assert.deepEqual(filterDatabaseNamesForConnection(["admin", "shop", "local"], { db_type: "mongodb" }), ["shop"]); + assert.deepEqual(filterDatabaseNamesForConnection(["SYS", "HR", "SYSTEM"], { db_type: "oracle" }), ["HR"]); + assert.deepEqual(filterDatabaseNamesForConnection(["_SYS_BIC", "SALES"], { db_type: "saphana" }), ["SALES"]); + assert.deepEqual(filterDatabaseNamesForConnection(["system_schema", "app"], { db_type: "cassandra" }), ["app"]); + assert.deepEqual(filterDatabaseNamesForConnection(["system", "neo4j"], { db_type: "neo4j" }), ["neo4j"]); + assert.deepEqual(filterDatabaseNamesForConnection(["SNOWFLAKE", "ANALYTICS"], { db_type: "snowflake" }), [ + "ANALYTICS", + ]); +});