From dff94d60f5ea504842b63202e98c659f10a991c0 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 26 Jun 2026 02:03:00 +0800 Subject: [PATCH] fix(oceanbase): show oracle mode schemas in sidebar --- .../connection/ConnectionDialog.vue | 8 +++---- .../sidebar/VisibleDatabasesDialog.vue | 4 ++-- .../src/composables/useSchemaOptions.ts | 8 +++---- apps/desktop/src/lib/visibleDatabases.ts | 9 ++++++- apps/desktop/src/stores/connectionStore.ts | 9 +++---- crates/dbx-core/src/db/ob_oracle.rs | 24 ++++++++++++------- crates/dbx-core/src/schema.rs | 3 +++ .../dbx-core/src/schema/providers/native.rs | 1 + .../connectionVisibleDatabases.test.ts | 6 +++++ packages/app-tests/schemaOptions.test.ts | 24 +++++++++++++++++++ 10 files changed, 73 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 9f8dbff2e..4aec6b6da 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -37,7 +37,7 @@ import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/databaseFileDetection"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connectionAttemptTimeout"; import { ArrowLeft, ArrowDown, ArrowUp, CheckSquare, ChevronRight, CircleHelp, Copy, ExternalLink, FilePlus2, FolderOpen, GripVertical, Grid3X3, KeyRound, Link2, List, ListFilter, Loader2, Pipette, Plus, Search, ShieldCheck, Square, Trash2 } from "@lucide/vue"; import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connectionVisibleDatabases"; -import { canSaveVisibleDatabaseSelection, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/visibleDatabases"; +import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/visibleDatabases"; import { isSchemaAware } from "@/lib/databaseFeatureSupport"; import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue"; import { oceanbaseModeConnectionPatch, oceanbaseSubModeFromConfig } from "@/lib/oceanbaseConnectionMode"; @@ -1323,7 +1323,7 @@ const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile)); const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true); const canChooseVisibleDatabases = computed(() => connectionCanChooseVisibleDatabases(form.value)); -const visibleFilterUsesSchemas = computed(() => form.value.db_type === "oracle" || form.value.db_type === "dameng"); +const visibleFilterUsesSchemas = computed(() => connectionUsesVisibleSchemaFilter(form.value)); const hasVisibleDatabaseFilter = computed(() => Array.isArray(form.value.visible_databases)); const visibleDatabaseSummary = computed(() => { const configured = form.value.visible_databases; @@ -1694,7 +1694,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { delete legacy.proxy_port; delete legacy.proxy_username; delete legacy.proxy_password; - if (config.db_type === "oracle" || config.db_type === "dameng") { + if (connectionUsesVisibleSchemaFilter(config)) { config.visible_databases = undefined; } else { config.visible_databases = Array.isArray(config.visible_databases) && config.visible_databases.length > 0 ? config.visible_databases : undefined; @@ -1983,7 +1983,7 @@ async function openVisibleDatabasesPicker() { } async function loadVisibleDatabaseNames(connectionId: string, config: ConnectionConfig): Promise { - if (config.db_type === "oracle" || config.db_type === "dameng") { + if (connectionUsesVisibleSchemaFilter(config)) { return api.listSchemas(connectionId, config.database || ""); } if (config.db_type === "redis") { diff --git a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue index 8d000411b..5322f7158 100644 --- a/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue +++ b/apps/desktop/src/components/sidebar/VisibleDatabasesDialog.vue @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { useConnectionStore } from "@/stores/connectionStore"; -import { canSaveVisibleDatabaseSelection, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, isSystemDatabaseName, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; import * as api from "@/lib/api"; const props = defineProps<{ @@ -32,7 +32,7 @@ const isLoading = ref(false); const errorMessage = ref(""); const connection = computed(() => connectionStore.getConfig(props.connectionId)); -const filterMode = computed(() => (connection.value?.db_type === "oracle" || connection.value?.db_type === "dameng" ? "schema" : "database")); +const filterMode = computed(() => (connectionUsesVisibleSchemaFilter(connection.value) ? "schema" : "database")); const databaseKey = computed(() => connection.value?.database || ""); const isSchemaFilterMode = computed(() => filterMode.value === "schema"); const titleKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.title" : "visibleDatabases.title")); diff --git a/apps/desktop/src/composables/useSchemaOptions.ts b/apps/desktop/src/composables/useSchemaOptions.ts index 07cf3f6ce..394d7de8c 100644 --- a/apps/desktop/src/composables/useSchemaOptions.ts +++ b/apps/desktop/src/composables/useSchemaOptions.ts @@ -1,7 +1,7 @@ import { ref } from "vue"; import { useConnectionStore } from "@/stores/connectionStore"; import { isSchemaAware as isSchemaAwareType, isSingleDatabase, usesTreeSchemaMode } from "@/lib/databaseCapabilities"; -import { filterDatabaseNamesForConnection } from "@/lib/visibleDatabases"; +import { filterSchemaNamesForConnection } from "@/lib/visibleDatabases"; import type { ConnectionConfig } from "@/types/database"; import * as api from "@/lib/api"; @@ -9,8 +9,8 @@ export function hasSchemaOptionsCacheEntry(options: Record, ke return Object.prototype.hasOwnProperty.call(options, key); } -export function schemaOptionsForConnection(schemaNames: string[], connection: Pick | undefined): string[] { - return filterDatabaseNamesForConnection(schemaNames, connection); +export function schemaOptionsForConnection(schemaNames: string[], connection: Pick | undefined, database = ""): string[] { + return filterSchemaNamesForConnection(schemaNames, connection, database); } export function useSchemaOptions() { @@ -39,7 +39,7 @@ export function useSchemaOptions() { loadingSchemaOptions.value[key] = true; try { await connectionStore.ensureConnected(connectionId); - schemaOptions.value[key] = schemaOptionsForConnection(await api.listSchemas(connectionId, database), connection); + schemaOptions.value[key] = schemaOptionsForConnection(await api.listSchemas(connectionId, database), connection, database); } catch (e) { schemaOptions.value[key] = []; throw e; diff --git a/apps/desktop/src/lib/visibleDatabases.ts b/apps/desktop/src/lib/visibleDatabases.ts index e7ad2cd4b..4f15b6653 100644 --- a/apps/desktop/src/lib/visibleDatabases.ts +++ b/apps/desktop/src/lib/visibleDatabases.ts @@ -99,13 +99,20 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name)); } +export function connectionUsesVisibleSchemaFilter(connection: Pick | undefined): boolean { + return connection?.db_type === "oracle" || connection?.db_type === "dameng" || connection?.db_type === "oceanbase-oracle"; +} + export function visibleSchemaFilterIsEnabled(visibleSchemas: Record | undefined, database: string): boolean { return Array.isArray(visibleSchemas?.[database]); } -export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick | undefined, database: string): string[] { +export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick | undefined, database: string): string[] { const visibleSchemas = connection?.visible_schemas; if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) { + if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) { + return filterVisibleDatabaseNames(schemaNames, connection?.visible_databases); + } return schemaNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name)); } const visible = new Set(visibleSchemas![database]); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index aeb2a1709..5054cdb1e 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -28,7 +28,7 @@ import { buildSqlServerDatabaseTreeNodes } from "@/lib/sqlServerTree"; import { findDatabaseTreeNode } from "@/lib/treeRefreshTarget"; import { shouldMarkDisconnected } from "@/lib/connectionHealth"; import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connectionAttemptTimeout"; -import { filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; +import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/visibleDatabases"; import { buildObjectGroupPlaceholderNodes, buildGroupedObjectTreeNodes, @@ -1235,8 +1235,9 @@ export const useConnectionStore = defineStore("connection", () => { const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node); setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); - } else if (config?.db_type === "dameng" || config?.db_type === "oracle") { - const effectiveDb = config.database || ""; + } else if (config && connectionUsesVisibleSchemaFilter(config)) { + const schemaFilterConfig = config; + const effectiveDb = schemaFilterConfig.database || ""; const cacheKey = schemaCacheKey(connectionId, effectiveDb, "schemas"); if (!options?.force) { const cached = await loadPersistedTreeChildren(node, cacheKey); @@ -1246,7 +1247,7 @@ export const useConnectionStore = defineStore("connection", () => { } } const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb, true), "schemas"); - const visibleSchemas = filterSchemaNamesForConnection(schemas, config, effectiveDb || ""); + const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || ""); const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({ id: `${connectionId}:${s}:${s}`, label: s, diff --git a/crates/dbx-core/src/db/ob_oracle.rs b/crates/dbx-core/src/db/ob_oracle.rs index afa04f4f9..52fab4219 100644 --- a/crates/dbx-core/src/db/ob_oracle.rs +++ b/crates/dbx-core/src/db/ob_oracle.rs @@ -29,21 +29,24 @@ fn get_opt_i32(row: &mysql_async::Row, idx: usize) -> Option { row_get::(row, idx).or_else(|| row_get::(row, idx).and_then(|v| i32::try_from(v).ok())) } +fn list_user_schemas_sql() -> &'static str { + "SELECT USERNAME FROM ALL_USERS \ + WHERE USERNAME NOT IN ('SYS','LBACSYS','__public') \ + ORDER BY USERNAME" +} + pub async fn list_databases(pool: &mysql_async::Pool) -> Result, String> { let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?; - let result = conn - .query_iter( - "SELECT USERNAME FROM ALL_USERS \ - WHERE USERNAME NOT IN ('SYS','LBACSYS','ORAAUDITOR','__public') \ - ORDER BY USERNAME", - ) - .await - .map_err(|e| e.to_string())?; + let result = conn.query_iter(list_user_schemas_sql()).await.map_err(|e| e.to_string())?; let rows: Vec = result.collect_and_drop().await.map_err(|e| e.to_string())?; Ok(rows.iter().map(|row| DatabaseInfo { name: get_str(row, 0) }).collect()) } +pub async fn list_schemas(pool: &mysql_async::Pool) -> Result, String> { + list_databases(pool).await.map(|databases| databases.into_iter().map(|database| database.name).collect()) +} + pub async fn list_tables(pool: &mysql_async::Pool, schema: &str) -> Result, String> { let sql = format!( "SELECT TABLE_NAME, 'TABLE' AS TABLE_TYPE FROM ALL_TABLES WHERE OWNER = {s} \ @@ -289,4 +292,9 @@ mod tests { assert!(sql.contains("'PACKAGE'")); assert!(sql.contains("'PACKAGE BODY'")); } + + #[test] + fn ob_oracle_user_schema_sql_keeps_auditor_schema_available() { + assert!(!list_user_schemas_sql().contains("ORAAUDITOR")); + } } diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 49e818461..1a780560e 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -714,6 +714,9 @@ async fn list_schemas_once( let pool = connections.get(&pool_key).ok_or("Pool not found")?; match pool { + PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => db::ob_oracle::list_schemas(p) + .await + .map(|schemas| filter_visible_schema_names(schemas, visible_schema_filter.as_deref())), PoolKind::Postgres(p) => db::postgres::list_schemas(p) .await .map(|schemas| filter_visible_schema_names(schemas, visible_schema_filter.as_deref())), diff --git a/crates/dbx-core/src/schema/providers/native.rs b/crates/dbx-core/src/schema/providers/native.rs index 74df2b872..f8c90894c 100644 --- a/crates/dbx-core/src/schema/providers/native.rs +++ b/crates/dbx-core/src/schema/providers/native.rs @@ -22,6 +22,7 @@ pub(in crate::schema) async fn list_databases( pub(in crate::schema) async fn list_schemas(pool: &PoolKind) -> Result, String> { match pool { + PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => db::ob_oracle::list_schemas(p).await, PoolKind::Postgres(p) => db::postgres::list_schemas(p).await, _ => Ok(vec![]), } diff --git a/packages/app-tests/connectionVisibleDatabases.test.ts b/packages/app-tests/connectionVisibleDatabases.test.ts index 8d74a0784..e074c827d 100644 --- a/packages/app-tests/connectionVisibleDatabases.test.ts +++ b/packages/app-tests/connectionVisibleDatabases.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { test } from "vitest"; import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connectionVisibleDatabases.ts"; +import { connectionUsesVisibleSchemaFilter } from "../../apps/desktop/src/lib/visibleDatabases.ts"; import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts"; function config(overrides: Partial = {}): ConnectionConfig { @@ -52,6 +53,11 @@ test("ZooKeeper connections do not offer visible database selection", () => { assert.equal(connectionCanChooseVisibleDatabases(config({ db_type: "zookeeper" })), false); }); +test("OceanBase Oracle uses schema filtering for visible object selection", () => { + assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "oceanbase-oracle" })), true); + assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "mysql", driver_profile: "oceanbase" })), false); +}); + test("visible database selection is stale when connection target changes", () => { const previous = config({ host: "db.internal", visible_databases: ["app"] }); assert.equal(visibleDatabaseSelectionIsStale(previous, config({ host: "db.internal" })), false); diff --git a/packages/app-tests/schemaOptions.test.ts b/packages/app-tests/schemaOptions.test.ts index eee844382..3ad7c85b0 100644 --- a/packages/app-tests/schemaOptions.test.ts +++ b/packages/app-tests/schemaOptions.test.ts @@ -21,3 +21,27 @@ test("schema options keep default system-schema filtering without explicit filte ["CAM"], ); }); + +test("schema options respect visible schemas for OceanBase Oracle mode", () => { + assert.deepEqual( + schemaOptionsForConnection( + ["ORAAUDITOR", "APP", "SYS"], + { + db_type: "oceanbase-oracle", + visible_schemas: { SYS: ["ORAAUDITOR"] }, + }, + "SYS", + ), + ["ORAAUDITOR"], + ); +}); + +test("schema options support legacy visible database filters for OceanBase Oracle mode", () => { + assert.deepEqual( + schemaOptionsForConnection(["ORAAUDITOR", "APP", "SYS"], { + db_type: "oceanbase-oracle", + visible_databases: ["ORAAUDITOR"], + }), + ["ORAAUDITOR"], + ); +});