feat(metadata): honor system schema visibility
This commit is contained in:
1 parent
a07def6042
commit
820b8cd26f
44 files changed
+422
-47
No files matched your search
@@ -255,6 +255,7 @@ const defaultForm = (): ConnectionForm => ({
|
||||
external_config: undefined,
|
||||
init_script: undefined,
|
||||
read_only: false,
|
||||
show_system_schemas: false,
|
||||
is_production: false,
|
||||
production_databases: [],
|
||||
visible_databases: undefined,
|
||||
@@ -1994,6 +1995,7 @@ watch(
|
||||
attached_databases: config.attached_databases || [],
|
||||
init_script: config.init_script,
|
||||
read_only: config.read_only || false,
|
||||
show_system_schemas: config.show_system_schemas || false,
|
||||
is_production: config.is_production || false,
|
||||
production_databases: config.production_databases || [],
|
||||
visible_databases: config.visible_databases,
|
||||
@@ -3391,6 +3393,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
} else {
|
||||
config.visible_databases = Array.isArray(config.visible_databases) && config.visible_databases.length > 0 ? config.visible_databases : undefined;
|
||||
}
|
||||
if (!config.show_system_schemas) config.show_system_schemas = undefined;
|
||||
if (config.visible_schemas && Object.keys(config.visible_schemas).length === 0) config.visible_schemas = undefined;
|
||||
if (config.agent_java_options && config.agent_java_options.length === 0) config.agent_java_options = undefined;
|
||||
return config as ConnectionConfig;
|
||||
@@ -6557,6 +6560,13 @@ function openExternalUrl(url: string) {
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.readOnlyHint") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isSchemaAware(form.db_type)" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.showSystemSchemas") }}</Label>
|
||||
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="form.show_system_schemas" class="mr-0" />
|
||||
<span class="text-xs text-muted-foreground">{{ t("connection.showSystemSchemasHint") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4 rounded-[6px] border border-red-500/25 bg-red-500/[0.035] px-3 py-2.5">
|
||||
<Label :class="[connectionLabelSmallClass, 'pt-0.5 text-red-700 dark:text-red-300']">
|
||||
<span class="inline-flex items-center justify-end gap-1"><ShieldAlert class="h-3.5 w-3.5" />PROD</span>
|
||||
|
||||
@@ -95,6 +95,7 @@ import { formatShortcut } from "@/lib/editor/shortcutRegistry";
|
||||
import { batchTableEmptyFeedback, buildBatchTableEmptyPlan, runBatchTableEmpty, type BatchTableEmptyPlanItem } from "@/lib/sidebar/batchTableEmpty";
|
||||
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { filterSchemaNamesForConnection } from "@/lib/database/visibleDatabases";
|
||||
import {
|
||||
buildObjectBrowserRows,
|
||||
countObjectBrowserRowsByFilter,
|
||||
@@ -415,6 +416,13 @@ watch(objectFilter, () => {
|
||||
}
|
||||
scrollObjectsToTop();
|
||||
});
|
||||
watch(
|
||||
() => props.connection.show_system_schemas,
|
||||
(value, oldValue) => {
|
||||
if (value === oldValue) return;
|
||||
void reload();
|
||||
},
|
||||
);
|
||||
|
||||
const showCheckboxColumn = computed(() => settingsStore.editorSettings.objectBrowserShowCheckbox || selectedTableCount.value > 0);
|
||||
|
||||
@@ -2186,9 +2194,15 @@ async function loadSchemas(epoch: number): Promise<boolean> {
|
||||
const connectionId = props.connection.id;
|
||||
const database = props.database;
|
||||
try {
|
||||
const names = await api.listSchemas(connectionId, database);
|
||||
const names = filterSchemaNamesForConnection(await api.listSchemas(connectionId, database), props.connection, database, {
|
||||
showSystemSchemas: props.connection.show_system_schemas === true,
|
||||
});
|
||||
if (!objectBrowserRowsLoadGuard.isEpochCurrent(epoch)) return false;
|
||||
schemas.value = names;
|
||||
if (names.length === 0) {
|
||||
selectedSchema.value = undefined;
|
||||
return true;
|
||||
}
|
||||
if (!selectedSchema.value || !names.includes(selectedSchema.value)) {
|
||||
selectedSchema.value = names.includes("public") ? "public" : names[0];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { schemaOptionsForConnection } from "@/composables/useSchemaOptions";
|
||||
import { schemaOptionsCacheKey, schemaOptionsForConnection } from "@/composables/useSchemaOptions";
|
||||
|
||||
describe("schemaOptionsForConnection", () => {
|
||||
it("sorts schema names with numeric suffixes naturally", () => {
|
||||
@@ -19,4 +19,19 @@ describe("schemaOptionsForConnection", () => {
|
||||
|
||||
expect(schemaOptionsForConnection(["WMWMSE10", "SYSTEM", "WMWMSE2", "WMWMSE1"], connection, "ORCL")).toEqual(["WMWMSE2", "WMWMSE10"]);
|
||||
});
|
||||
|
||||
it.each(["opengauss", "kingbase"] as const)("includes system schemas for %s when enabled", (dbType) => {
|
||||
expect(
|
||||
schemaOptionsForConnection(["information_schema", "pg_catalog", "public"], {
|
||||
db_type: dbType,
|
||||
show_system_schemas: true,
|
||||
}),
|
||||
).toEqual(["information_schema", "pg_catalog", "public"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("schemaOptionsCacheKey", () => {
|
||||
it("separates hidden and visible system-schema results", () => {
|
||||
expect(schemaOptionsCacheKey("connection-1", "postgres", false)).not.toBe(schemaOptionsCacheKey("connection-1", "postgres", true));
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,11 @@ 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" | "visible_schemas"> | undefined, database = ""): string[] {
|
||||
export function schemaOptionsCacheKey(connectionId: string, database: string, showSystemSchemas: boolean): string {
|
||||
return `${connectionId}:${database}:${showSystemSchemas ? "show-system" : "hide-system"}`;
|
||||
}
|
||||
|
||||
export function schemaOptionsForConnection(schemaNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases" | "visible_schemas" | "show_system_schemas"> | undefined, database = ""): string[] {
|
||||
// Keep numeric schema suffixes in human order (SCHEMA2 before SCHEMA10), matching the database tree.
|
||||
return sortSidebarNames(filterSchemaNamesForConnection(schemaNames, connection, database));
|
||||
}
|
||||
@@ -22,7 +26,7 @@ export function useSchemaOptions() {
|
||||
const loadingSchemaOptions = ref<Record<string, boolean>>({});
|
||||
|
||||
function cacheKey(connectionId: string, database: string) {
|
||||
return `${connectionId}:${database}`;
|
||||
return schemaOptionsCacheKey(connectionId, database, connectionStore.getConfig(connectionId)?.show_system_schemas === true);
|
||||
}
|
||||
|
||||
function isSchemaAware(connectionId: string): boolean {
|
||||
|
||||
@@ -627,6 +627,8 @@ export default {
|
||||
keepaliveInterval: "Keepalive Interval (seconds)",
|
||||
readOnly: "Read Only",
|
||||
readOnlyHint: "Block all write operations (INSERT, UPDATE, DELETE, etc.)",
|
||||
showSystemSchemas: "Show System Schemas",
|
||||
showSystemSchemasHint: "Show built-in and metadata schemas in the sidebar and schema pickers for this connection.",
|
||||
readOnlyBadge: "Read-only",
|
||||
proxy: "Proxy",
|
||||
proxyEnable: "Connect database through proxy",
|
||||
|
||||
@@ -659,6 +659,8 @@ export default withEnglishFallback({
|
||||
rocketmqSecretKey: "Secret Key",
|
||||
rocketmqAclAuth: "ACL",
|
||||
jdbcMissingRuntimeDependencyHint: "El controlador JDBC actual carece de dependencias de ejecución. Utilice las coordenadas Maven en 'Administración de controladores' para instalar, o importe el controlador y todos los JAR de dependencia de una vez.",
|
||||
showSystemSchemas: "Mostrar Schema del sistema",
|
||||
showSystemSchemasHint: "Mostrar el Schema integrado/de metadatos para la conexión actual en la barra lateral y el selector de Schema.",
|
||||
sshHostKeyVerifyTitle: "Confirmar clave de host SSH desconocida",
|
||||
sshHostKeyVerifyMessage: "No se puede confirmar la autenticidad del host '{host}:{port}'. Para evitar ataques de intermediario, verifique la huella digital de la clave del host con el administrador del servidor antes de continuar.",
|
||||
sshHostKeyVerifyKeyType: "Tipo de clave",
|
||||
|
||||
@@ -657,6 +657,8 @@ export default withEnglishFallback({
|
||||
rocketmqSecretKey: "Secret Key",
|
||||
rocketmqAclAuth: "ACL",
|
||||
jdbcMissingRuntimeDependencyHint: "Il driver JDBC corrente manca di dipendenze runtime. Installare utilizzando le coordinate Maven in 'Gestione driver', o importare il driver e tutti i JAR delle dipendenze in una volta.",
|
||||
showSystemSchemas: "Mostra Schema di sistema",
|
||||
showSystemSchemasHint: "Mostra lo Schema built-in/di metadati nella barra laterale e nel selettore Schema per la connessione corrente.",
|
||||
sshHostKeyVerifyTitle: "Conferma chiave host SSH sconosciuta",
|
||||
sshHostKeyVerifyMessage: "Impossibile confermare l'autenticità dell'host '{host}:{port}'. Per prevenire attacchi man-in-the-middle, verifica l'impronta della chiave host con l'amministratore del server prima di procedere.",
|
||||
sshHostKeyVerifyKeyType: "Tipo di chiave",
|
||||
|
||||
@@ -657,6 +657,8 @@ export default withEnglishFallback({
|
||||
rocketmqSecretKey: "Secret Key",
|
||||
rocketmqAclAuth: "ACL",
|
||||
jdbcMissingRuntimeDependencyHint: "現在のJDBCドライバーには実行依存関係が不足しています。「ドライバ管理」でMaven座標を使用してインストールするか、ドライバーとすべての依存JARを一度にインポートしてください。",
|
||||
showSystemSchemas: "システムスキーマを表示",
|
||||
showSystemSchemasHint: "現在の接続で、サイドバーとスキーマセレクターに組み込み/メタデータスキーマを表示します。",
|
||||
sshHostKeyVerifyTitle: "不明なSSHホストキーの確認",
|
||||
sshHostKeyVerifyMessage: "ホスト '{host}:{port}' の正当性を確認できません。中間者攻撃を防ぐため、続行する前にサーバー管理者にホストキーのフィンガープリントを確認してください。",
|
||||
sshHostKeyVerifyKeyType: "キータイプ",
|
||||
|
||||
@@ -658,6 +658,8 @@ export default withEnglishFallback({
|
||||
rocketmqSecretKey: "Secret Key",
|
||||
rocketmqAclAuth: "ACL",
|
||||
jdbcMissingRuntimeDependencyHint: "O driver JDBC atual está sem dependências de execução. Por favor, instale-o usando as coordenadas Maven no 'Gerenciamento de Drivers' ou importe o driver e todos os JARs de dependência de uma só vez.",
|
||||
showSystemSchemas: "Mostrar Schema do sistema",
|
||||
showSystemSchemasHint: "Exibir Schemas de sistema/metadados integrados na barra lateral e no seletor de Schema para a conexão atual.",
|
||||
sshHostKeyVerifyTitle: "Confirmar chave de host SSH desconhecida",
|
||||
sshHostKeyVerifyMessage: "Não foi possível confirmar a autenticidade do host '{host}:{port}'. Para evitar ataques man-in-the-middle, verifique a impressão digital da chave do host com o administrador do servidor antes de continuar.",
|
||||
sshHostKeyVerifyKeyType: "Tipo de chave",
|
||||
|
||||
@@ -630,6 +630,8 @@ export default withEnglishFallback({
|
||||
keepaliveInterval: "保持连接间隔(秒)",
|
||||
readOnly: "只读模式",
|
||||
readOnlyHint: "阻止所有写操作(INSERT、UPDATE、DELETE 等)",
|
||||
showSystemSchemas: "显示系统 Schema",
|
||||
showSystemSchemasHint: "为当前连接在侧边栏和 Schema 选择器中显示内置/元数据 Schema。",
|
||||
readOnlyBadge: "只读",
|
||||
proxy: "代理",
|
||||
proxyEnable: "通过代理连接数据库",
|
||||
|
||||
@@ -657,6 +657,8 @@ export default withEnglishFallback({
|
||||
rocketmqSecretKey: "Secret Key",
|
||||
rocketmqAclAuth: "ACL",
|
||||
jdbcMissingRuntimeDependencyHint: "目前 JDBC 驅動缺少執行依賴。請在「驅動管理」中使用 Maven 座標安裝,或一次匯入驅動及全部依賴 JAR。",
|
||||
showSystemSchemas: "顯示系統 Schema",
|
||||
showSystemSchemasHint: "為目前連線在側邊欄和 Schema 選擇器中顯示內建/中繼資料 Schema。",
|
||||
sshHostKeyVerifyTitle: "確認未知的 SSH 主機金鑰",
|
||||
sshHostKeyVerifyMessage: "無法確認主機 '{host}:{port}' 的真實性。為防止中間人攻擊,請在繼續前與伺服器管理員核對主機金鑰指紋。",
|
||||
sshHostKeyVerifyKeyType: "金鑰類型",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterSchemaNamesForConnection, filterSchemaNamesForVisiblePicker, isSystemSchemaName } from "@/lib/database/visibleDatabases";
|
||||
|
||||
describe("visibleDatabases schema filtering", () => {
|
||||
it("hides common Kingbase system schemas by default", () => {
|
||||
expect(filterSchemaNamesForVisiblePicker(["anon", "dbms_job", "information_schema", "pg_catalog", "public", "sys", "sys_catalog", "wmsys", "xlog_record_read"], { db_type: "kingbase", username: "test" })).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("keeps the current schema visible even when it matches a system schema name", () => {
|
||||
expect(
|
||||
filterSchemaNamesForVisiblePicker(["public", "sys", "sys_catalog"], {
|
||||
db_type: "kingbase",
|
||||
username: "sys",
|
||||
}),
|
||||
).toEqual(["public", "sys"]);
|
||||
});
|
||||
|
||||
it("keeps Oracle DIP visible while hiding default system schemas", () => {
|
||||
expect(filterSchemaNamesForConnection(["DBX_TEST", "DIP", "SYSTEM"], { db_type: "oracle", database: "XE" }, "XE")).toEqual(["DBX_TEST", "DIP"]);
|
||||
});
|
||||
|
||||
it("keeps the Dameng login schema visible while hiding default system schemas", () => {
|
||||
expect(filterSchemaNamesForConnection(["APP", "SYS", "SYSDBA", "SYSDBO", "SYSAUDITOR"], { db_type: "dameng", username: "SYSDBA" }, "")).toEqual(["APP", "SYSDBA"]);
|
||||
});
|
||||
|
||||
it("hides openGauss system schemas and prefixes while keeping user schemas", () => {
|
||||
expect(filterSchemaNamesForVisiblePicker(["blockchain", "cstore", "db4ai", "dbe_perf", "dbe_pldeveloper", "dbe_sql_util", "information_schema", "pg_catalog", "public", "snapshot", "sqladvisor", "xmltype"], { db_type: "opengauss", username: "app_user" })).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("keeps all schemas visible when show-system-schemas is enabled", () => {
|
||||
expect(filterSchemaNamesForConnection(["blockchain", "db4ai", "public", "test2", "xmltype"], { db_type: "opengauss", show_system_schemas: true }, "postgres")).toEqual(["blockchain", "db4ai", "public", "test2", "xmltype"]);
|
||||
});
|
||||
|
||||
it("respects explicit visible schema configuration after default filtering", () => {
|
||||
expect(
|
||||
filterSchemaNamesForConnection(
|
||||
["public", "sys_catalog", "reporting"],
|
||||
{
|
||||
db_type: "kingbase",
|
||||
visible_schemas: { test: ["sys_catalog"] },
|
||||
},
|
||||
"test",
|
||||
),
|
||||
).toEqual(["sys_catalog"]);
|
||||
});
|
||||
|
||||
it("matches prefix-based system schema rules", () => {
|
||||
expect(isSystemSchemaName("kingbase", "xlog_record_read")).toBe(true);
|
||||
expect(isSystemSchemaName("opengauss", "dbe_pldeveloper")).toBe(true);
|
||||
expect(isSystemSchemaName("kingbase", "public")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,18 @@
|
||||
import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
|
||||
type SystemNameRules = {
|
||||
exact?: ReadonlySet<string>;
|
||||
prefixes?: readonly string[];
|
||||
};
|
||||
|
||||
type SchemaFilterOptions = {
|
||||
showSystemSchemas?: boolean;
|
||||
};
|
||||
|
||||
function schemaFilterShowSystemSchemas(connection: Partial<Pick<ConnectionConfig, "show_system_schemas">> | undefined, options?: SchemaFilterOptions): boolean {
|
||||
return options?.showSystemSchemas ?? connection?.show_system_schemas === true;
|
||||
}
|
||||
|
||||
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"]),
|
||||
@@ -59,6 +72,67 @@ const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>>
|
||||
snowflake: new Set(["snowflake", "snowflake_sample_data"]),
|
||||
};
|
||||
|
||||
const POSTGRES_LIKE_SYSTEM_SCHEMA_RULES: SystemNameRules = {
|
||||
exact: new Set(["information_schema", "pg_catalog", "pg_toast"]),
|
||||
prefixes: ["pg_temp_", "pg_toast_temp_"],
|
||||
};
|
||||
|
||||
const SYSTEM_SCHEMA_RULES: Partial<Record<DatabaseType, SystemNameRules>> = {
|
||||
oracle: {
|
||||
exact: new Set([
|
||||
"anonymous",
|
||||
"appqossys",
|
||||
"audsys",
|
||||
"ctxsys",
|
||||
"dbsnmp",
|
||||
"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: {
|
||||
exact: new Set(["_sys_statistics", "ctisys", "dba", "sys", "sys_dba", "sys_phm", "sysauditor", "sysdba", "sysdbo", "syssso", "system"]),
|
||||
},
|
||||
postgres: POSTGRES_LIKE_SYSTEM_SCHEMA_RULES,
|
||||
gaussdb: {
|
||||
exact: new Set(["blockchain", "coverage", "cstore", "db4ai", "dbe_perf", "dbe_pldebugger", "dbe_pldeveloper", "dbe_sql_util", "information_schema", "pg_catalog", "pg_toast", "pkg_service", "snapshot", "sqladvisor", "xmltype"]),
|
||||
prefixes: ["pg_temp_", "pg_toast_temp_", "dbe_"],
|
||||
},
|
||||
kwdb: POSTGRES_LIKE_SYSTEM_SCHEMA_RULES,
|
||||
opengauss: {
|
||||
exact: new Set(["blockchain", "coverage", "cstore", "db4ai", "dbe_perf", "dbe_pldebugger", "dbe_pldeveloper", "dbe_sql_util", "information_schema", "pg_catalog", "pg_toast", "pkg_service", "snapshot", "sqladvisor", "xmltype"]),
|
||||
prefixes: ["pg_temp_", "pg_toast_temp_", "dbe_"],
|
||||
},
|
||||
questdb: POSTGRES_LIKE_SYSTEM_SCHEMA_RULES,
|
||||
kingbase: {
|
||||
exact: new Set(["anon", "dbms_job", "dbms_scheduler", "dbms_sql", "information_schema", "kdb_schedule", "perf", "pg_bitmapindex", "pg_catalog", "pg_toast", "src_restrict", "sys", "sys_catalog", "sys_hm", "sysaudit", "sysmac", "wmsys"]),
|
||||
prefixes: ["dbms_", "pg_temp_", "pg_toast_temp_", "sys_temp_", "sys_toast_temp_", "xlog_"],
|
||||
},
|
||||
highgo: POSTGRES_LIKE_SYSTEM_SCHEMA_RULES,
|
||||
vastbase: POSTGRES_LIKE_SYSTEM_SCHEMA_RULES,
|
||||
};
|
||||
|
||||
export function visibleDatabaseFilterIsEnabled(visibleDatabases: string[] | undefined): boolean {
|
||||
return Array.isArray(visibleDatabases);
|
||||
}
|
||||
@@ -88,6 +162,15 @@ export function isSystemDatabaseName(databaseType: DatabaseType | undefined, dat
|
||||
return SYSTEM_DATABASE_RULES[databaseType]?.has(databaseName.toLowerCase()) ?? false;
|
||||
}
|
||||
|
||||
export function isSystemSchemaName(databaseType: DatabaseType | undefined, schemaName: string): boolean {
|
||||
if (!databaseType) return false;
|
||||
const normalized = schemaName.toLowerCase();
|
||||
const rules = SYSTEM_SCHEMA_RULES[databaseType];
|
||||
if (!rules) return false;
|
||||
if (rules.exact?.has(normalized)) return true;
|
||||
return rules.prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
|
||||
}
|
||||
|
||||
export function filterDatabaseNamesForConnection(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases"> | undefined): string[] {
|
||||
const visibleDatabases = connection?.visible_databases;
|
||||
if (visibleDatabaseFilterIsEnabled(visibleDatabases)) {
|
||||
@@ -103,9 +186,10 @@ export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], con
|
||||
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
|
||||
}
|
||||
|
||||
export function filterSchemaNamesForVisiblePicker(schemaNames: string[], connection: Partial<Pick<ConnectionConfig, "db_type" | "username">> | undefined): string[] {
|
||||
export function filterSchemaNamesForVisiblePicker(schemaNames: string[], connection: Partial<Pick<ConnectionConfig, "db_type" | "username" | "show_system_schemas">> | undefined, options?: SchemaFilterOptions): string[] {
|
||||
if (schemaFilterShowSystemSchemas(connection, options)) return schemaNames;
|
||||
const currentSchema = connection?.username?.trim().toLowerCase();
|
||||
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !isSystemDatabaseName(connection?.db_type, name));
|
||||
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !isSystemSchemaName(connection?.db_type, name));
|
||||
}
|
||||
|
||||
export function connectionUsesVisibleSchemaFilter(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
|
||||
@@ -116,13 +200,18 @@ export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, stri
|
||||
return Array.isArray(visibleSchemas?.[database]);
|
||||
}
|
||||
|
||||
export function filterSchemaNamesForConnection(schemaNames: string[], connection: (Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> & Partial<Pick<ConnectionConfig, "username">>) | undefined, database: string): string[] {
|
||||
export function filterSchemaNamesForConnection(
|
||||
schemaNames: string[],
|
||||
connection: (Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases" | "show_system_schemas"> & Partial<Pick<ConnectionConfig, "username">>) | undefined,
|
||||
database: string,
|
||||
options?: SchemaFilterOptions,
|
||||
): string[] {
|
||||
const visibleSchemas = connection?.visible_schemas;
|
||||
if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) {
|
||||
if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) {
|
||||
return filterVisibleDatabaseNames(schemaNames, connection?.visible_databases);
|
||||
}
|
||||
return filterSchemaNamesForVisiblePicker(schemaNames, connection);
|
||||
return filterSchemaNamesForVisiblePicker(schemaNames, connection, options);
|
||||
}
|
||||
const visible = new Set(visibleSchemas![database]);
|
||||
return schemaNames.filter((name) => visible.has(name));
|
||||
|
||||
@@ -543,6 +543,64 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(store.treeNodes[0]?.children?.[0]?.children?.map((node) => node.label)).toEqual(["public", "tree.extensions"]);
|
||||
});
|
||||
|
||||
it.each(["opengauss", "kingbase"] as const)("reloads %s sidebar schemas when system visibility changes", async (dbType) => {
|
||||
const listSchemaInfos = vi.fn().mockResolvedValue([
|
||||
{ name: "information_schema", comment: null },
|
||||
{ name: "pg_catalog", comment: null },
|
||||
{ name: "public", comment: null },
|
||||
]);
|
||||
const loadSchemaCache = vi.fn().mockResolvedValue(null);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache,
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = { ...postgresConnection(), id: `${dbType}-1`, db_type: dbType, show_system_schemas: false } as ConnectionConfig;
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const databaseNode = store.treeNodes[0]!.children![0]!;
|
||||
|
||||
await store.loadSchemas(connection.id, "app");
|
||||
expect(databaseNode.children?.map((node) => node.label).filter((label) => label !== "tree.extensions")).toEqual(["public"]);
|
||||
|
||||
store.connections[0]!.show_system_schemas = true;
|
||||
databaseNode.children = [];
|
||||
databaseNode.isExpanded = false;
|
||||
await store.loadSchemas(connection.id, "app");
|
||||
|
||||
expect(loadSchemaCache.mock.calls.map(([key]) => key)).toEqual([`${connection.id}:app:schemas-v3:hide-system`, `${connection.id}:app:schemas-v3:show-system`]);
|
||||
expect(databaseNode.children?.map((node) => node.label).filter((label) => label !== "tree.extensions")).toEqual(["information_schema", "pg_catalog", "public"]);
|
||||
});
|
||||
|
||||
it("clears a failed metadata warning when the driver hint finishes during retry", async () => {
|
||||
let resolveAgents!: (drivers: Array<{ db_type: string; installed: boolean; update_available: boolean }>) => void;
|
||||
let resolveSchemas!: (schemas: Array<{ name: string; comment: null }>) => void;
|
||||
|
||||
@@ -1028,6 +1028,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
attached_databases: Array.isArray(config.attached_databases) ? config.attached_databases.filter((database) => database.name?.trim() && database.path?.trim()) : [],
|
||||
init_script: config.init_script?.trim() ? config.init_script : undefined,
|
||||
transport_layers: Array.isArray(config.transport_layers) ? config.transport_layers : [],
|
||||
show_system_schemas: config.show_system_schemas === true,
|
||||
connect_timeout_secs: config.connect_timeout_secs || 10,
|
||||
query_timeout_secs: config.query_timeout_secs ?? 30,
|
||||
idle_timeout_secs: config.idle_timeout_secs ?? 60,
|
||||
@@ -2870,7 +2871,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
} else if (config && connectionUsesVisibleSchemaFilter(config)) {
|
||||
const schemaFilterConfig = config;
|
||||
const effectiveDb = schemaFilterConfig.database || "";
|
||||
const cacheKey = schemaCacheKey(connectionId, effectiveDb, config.db_type === "oracle" ? "schemas-v2" : "schemas");
|
||||
const showSystemSchemas = schemaFilterConfig.show_system_schemas === true;
|
||||
const cacheKey = schemaCacheKey(connectionId, effectiveDb, config.db_type === "oracle" ? "schemas-v2" : "schemas", showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
@@ -2879,7 +2881,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
const schemas = await withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, effectiveDb, true), "schemas");
|
||||
const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || "");
|
||||
const visibleSchemas = filterSchemaNamesForConnection(schemas, schemaFilterConfig, effectiveDb || "", { showSystemSchemas });
|
||||
const schemaNodes: TreeNode[] = sortSidebarNames(visibleSchemas).map((s) => ({
|
||||
id: `${connectionId}:${s}:${s}`,
|
||||
label: s,
|
||||
@@ -3497,7 +3499,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, "schemas-v3");
|
||||
const showSystemSchemas = getConfig(connectionId)?.show_system_schemas === true;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, "schemas-v3", showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
@@ -3512,6 +3515,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
schemas.map((schema) => schema.name),
|
||||
getConfig(connectionId),
|
||||
database,
|
||||
{ showSystemSchemas },
|
||||
),
|
||||
);
|
||||
const children: TreeNode[] = schemas
|
||||
@@ -3567,7 +3571,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
|
||||
const cacheKey = schemaCacheKey(connectionId, database, simpleObjectDisplay ? "sqlserver-schemas-simple-v4" : "sqlserver-schemas-grouped-v4");
|
||||
const config = getConfig(connectionId);
|
||||
const showSystemSchemas = config?.show_system_schemas === true;
|
||||
const cacheKey = schemaCacheKey(connectionId, database, simpleObjectDisplay ? "sqlserver-schemas-simple-v4" : "sqlserver-schemas-grouped-v4", showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
if (cached.hit) {
|
||||
@@ -3575,9 +3581,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const config = getConfig(connectionId);
|
||||
const schemas = filterSchemaNamesForConnection(await api.listSchemas(connectionId, database), config, database);
|
||||
const schemas = filterSchemaNamesForConnection(await api.listSchemas(connectionId, database), config, database, { showSystemSchemas });
|
||||
const children = buildSqlServerDatabaseTreeNodes(connectionId, database, schemas);
|
||||
if (isSidebarSearchQueryChanged(options)) return;
|
||||
const targetNode = treeNodeLoadTarget(load);
|
||||
|
||||
@@ -131,6 +131,7 @@ export interface ConnectionConfig {
|
||||
database?: string;
|
||||
visible_databases?: string[];
|
||||
visible_schemas?: Record<string, string[]>;
|
||||
show_system_schemas?: boolean;
|
||||
attached_databases?: AttachedDatabaseConfig[];
|
||||
init_script?: string;
|
||||
color?: string;
|
||||
|
||||
Reference in new issue
Block a user