fix: let users opt into system databases

This commit is contained in:
t8y2 2026-05-26 12:06:48 +08:00
parent 7bdacc52ba
commit 76a0b9f76d
11 changed files with 190 additions and 35 deletions

View File

@ -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<string[]>([]);
const selectedNames = ref<Set<string>>(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<string[]> {
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() {
</div>
</div>
<label
v-if="hasSystemDatabases"
class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground"
>
<input
v-model="showSystemDatabases"
type="checkbox"
class="h-3.5 w-3.5 accent-primary"
:disabled="isLoading || !!errorMessage"
/>
<span>{{ t("visibleDatabases.showSystemDatabases") }}</span>
</label>
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
<div v-if="isLoading" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />

View File

@ -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;

View File

@ -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}",
},

View File

@ -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}",
},

View File

@ -835,6 +835,7 @@ export default {
selectAll: "全选",
clear: "清空",
showAll: "显示全部",
showSystemDatabases: "显示系统库",
save: "保存",
loadFailed: "加载数据库失败:{message}",
},

View File

@ -1,3 +1,68 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>> = {
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<ConnectionConfig, "db_type" | "visible_databases"> | undefined,
): string[] {
const visibleDatabases = connection?.visible_databases;
if (visibleDatabaseFilterIsEnabled(visibleDatabases)) {
return filterVisibleDatabaseNames(databaseNames, visibleDatabases);
}
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
}

View File

@ -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(

View File

@ -172,14 +172,7 @@ pub async fn test_connection(client: &ChClient) -> Result<(), String> {
}
pub async fn list_databases(client: &ChClient) -> Result<Vec<DatabaseInfo>, 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())
}

View File

@ -274,11 +274,7 @@ pub async fn connect_bare(url: &str) -> Result<MySqlPool, String> {
pub async fn list_databases(pool: &MySqlPool) -> Result<Vec<DatabaseInfo>, 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<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;

View File

@ -261,8 +261,7 @@ pub async fn list_databases(client: &mut SqlServerClient) -> Result<Vec<Database
.query(
"SELECT name \
FROM sys.databases \
WHERE database_id > 4 \
AND state = 0 \
WHERE state = 0 \
ORDER BY name",
&[],
)

View File

@ -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",
]);
});