fix(oceanbase): show oracle mode schemas in sidebar

This commit is contained in:
t8y2 2026-06-26 02:03:00 +08:00
parent 2dc71548c3
commit dff94d60f5
10 changed files with 73 additions and 23 deletions

View File

@ -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<string[]> {
if (config.db_type === "oracle" || config.db_type === "dameng") {
if (connectionUsesVisibleSchemaFilter(config)) {
return api.listSchemas(connectionId, config.database || "");
}
if (config.db_type === "redis") {

View File

@ -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<FilterMode>(() => (connection.value?.db_type === "oracle" || connection.value?.db_type === "dameng" ? "schema" : "database"));
const filterMode = computed<FilterMode>(() => (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"));

View File

@ -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<string, string[]>, ke
return Object.prototype.hasOwnProperty.call(options, key);
}
export function schemaOptionsForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases"> | undefined): string[] {
return filterDatabaseNamesForConnection(schemaNames, connection);
export function schemaOptionsForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases" | "visible_schemas"> | 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;

View File

@ -99,13 +99,20 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
}
export function connectionUsesVisibleSchemaFilter(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
return connection?.db_type === "oracle" || connection?.db_type === "dameng" || connection?.db_type === "oceanbase-oracle";
}
export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, string[]> | undefined, database: string): boolean {
return Array.isArray(visibleSchemas?.[database]);
}
export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_schemas"> | undefined, database: string): string[] {
export function filterSchemaNamesForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> | 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]);

View File

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

View File

@ -29,21 +29,24 @@ fn get_opt_i32(row: &mysql_async::Row, idx: usize) -> Option<i32> {
row_get::<i32, _>(row, idx).or_else(|| row_get::<i64, _>(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<Vec<DatabaseInfo>, 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<mysql_async::Row> = 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<Vec<String>, 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<Vec<TableInfo>, 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"));
}
}

View File

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

View File

@ -22,6 +22,7 @@ pub(in crate::schema) async fn list_databases(
pub(in crate::schema) async fn list_schemas(pool: &PoolKind) -> Result<Vec<String>, 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![]),
}

View File

@ -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> = {}): 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);

View File

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