fix: let users opt into system databases
This commit is contained in:
parent
7bdacc52ba
commit
76a0b9f76d
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -835,6 +835,7 @@ export default {
|
|||
selectAll: "全选",
|
||||
clear: "清空",
|
||||
showAll: "显示全部",
|
||||
showSystemDatabases: "显示系统库",
|
||||
save: "保存",
|
||||
loadFailed: "加载数据库失败:{message}",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())?;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
&[],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue