feat(metadata): honor system schema visibility
This commit is contained in:
parent
a07def6042
commit
820b8cd26f
|
|
@ -215,13 +215,27 @@ func (s *server) listDatabases() ([]databaseInfo, error) {
|
|||
return []databaseInfo{{Name: s.params.Database}}, nil
|
||||
}
|
||||
|
||||
func (s *server) listSchemas(visible []string) ([]string, error) {
|
||||
query := "SELECT nspname FROM sys_catalog.sys_namespace WHERE nspname NOT LIKE 'sys_temp_%' AND nspname NOT LIKE 'sys_toast_temp_%' ORDER BY nspname"
|
||||
if s.mode.postgresCatalog {
|
||||
query = "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg_temp_%' AND nspname NOT LIKE 'pg_toast_temp_%' ORDER BY nspname"
|
||||
} else if s.mode.mysqlCompat {
|
||||
query = kingbaseMySQLCompatListSchemasSQL
|
||||
func kingbaseListSchemasSQL(mode kingbaseMode, showSystemSchemas bool) string {
|
||||
if mode.mysqlCompat {
|
||||
if showSystemSchemas {
|
||||
return "SELECT schema_name FROM information_schema.schemata ORDER BY schema_name"
|
||||
}
|
||||
return kingbaseMySQLCompatListSchemasSQL
|
||||
}
|
||||
if mode.postgresCatalog {
|
||||
if showSystemSchemas {
|
||||
return "SELECT nspname FROM pg_catalog.pg_namespace ORDER BY nspname"
|
||||
}
|
||||
return "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg_temp_%' AND nspname NOT LIKE 'pg_toast_temp_%' ORDER BY nspname"
|
||||
}
|
||||
if showSystemSchemas {
|
||||
return "SELECT nspname FROM sys_catalog.sys_namespace ORDER BY nspname"
|
||||
}
|
||||
return "SELECT nspname FROM sys_catalog.sys_namespace WHERE nspname NOT LIKE 'sys_temp_%' AND nspname NOT LIKE 'sys_toast_temp_%' ORDER BY nspname"
|
||||
}
|
||||
|
||||
func (s *server) listSchemas(visible []string, showSystemSchemas bool) ([]string, error) {
|
||||
query := kingbaseListSchemasSQL(s.mode, showSystemSchemas)
|
||||
rows, err := s.metadataQuery(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -383,7 +397,7 @@ func (s *server) completionAssistantSearch(request completionAssistantRequest) (
|
|||
} else {
|
||||
schemas := []string{request.Schema}
|
||||
if request.GlobalSearch {
|
||||
visible, err := s.listSchemas(nil)
|
||||
visible, err := s.listSchemas(nil, false)
|
||||
if err != nil {
|
||||
return completionAssistantResponse{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ func (s *server) dispatch(method string, params map[string]json.RawMessage) (any
|
|||
result, err := s.listDatabases()
|
||||
return result, false, err
|
||||
case "list_schemas":
|
||||
result, err := s.listSchemas(stringSliceParam(params, "visible_schemas"))
|
||||
result, err := s.listSchemas(stringSliceParam(params, "visible_schemas"), boolParam(params, "show_system_schemas"))
|
||||
return result, false, err
|
||||
case "list_tables":
|
||||
result, err := s.listTables(stringParam(params, "schema"), metadataListConstraintsFromParams(params))
|
||||
|
|
@ -1138,6 +1138,14 @@ func intParam(params map[string]json.RawMessage, key string) int {
|
|||
return value
|
||||
}
|
||||
|
||||
func boolParam(params map[string]json.RawMessage, key string) bool {
|
||||
var value bool
|
||||
if raw, ok := params[key]; ok {
|
||||
_ = json.Unmarshal(raw, &value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func stringSliceParam(params map[string]json.RawMessage, key string) []string {
|
||||
var values []string
|
||||
if json.Unmarshal(params[key], &values) == nil {
|
||||
|
|
|
|||
|
|
@ -706,6 +706,24 @@ func TestMySQLCompatSchemaQueryKeepsUserSchemasWithSystemLikeNames(t *testing.T)
|
|||
}
|
||||
}
|
||||
|
||||
func TestListSchemasQueryIncludesSystemSchemasWhenEnabled(t *testing.T) {
|
||||
for _, mode := range []kingbaseMode{{}, {postgresCatalog: true}, {mysqlCompat: true}} {
|
||||
query := kingbaseListSchemasSQL(mode, true)
|
||||
if strings.Contains(query, "NOT LIKE") || strings.Contains(query, "<>") {
|
||||
t.Fatalf("show-system query must not filter schemas: %s", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSchemasQueryKeepsDefaultTemporarySchemaFilters(t *testing.T) {
|
||||
for _, mode := range []kingbaseMode{{}, {postgresCatalog: true}} {
|
||||
query := kingbaseListSchemasSQL(mode, false)
|
||||
if !strings.Contains(query, "temp_%") {
|
||||
t.Fatalf("default query must keep temporary schema filters: %s", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataNormalizationHelpers(t *testing.T) {
|
||||
if normalizeTableType("BASE TABLE") != "TABLE" {
|
||||
t.Fatal("BASE TABLE was not normalized")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -620,6 +620,7 @@ mod tests {
|
|||
database: database.map(str::to_string),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,7 @@ mod tests {
|
|||
database: Some("app_db".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
@ -1188,6 +1189,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
@ -1280,6 +1282,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: Some("CREATE SECRET (TYPE quack, TOKEN 'token-value');".to_string()),
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -3912,6 +3912,7 @@ mod tests {
|
|||
database: database.map(str::to_string),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -750,6 +750,7 @@ mod tests {
|
|||
database: Some("postgres".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -1048,19 +1048,23 @@ impl AgentDriverClient {
|
|||
database: &str,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.list_schemas_filtered(database, None, timeout_duration).await
|
||||
self.list_schemas_filtered(database, None, false, timeout_duration).await
|
||||
}
|
||||
|
||||
pub async fn list_schemas_filtered<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
database: &str,
|
||||
visible_schemas: Option<&[String]>,
|
||||
show_system_schemas: bool,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
let mut params = serde_json::json!({ "database": database });
|
||||
if let Some(visible_schemas) = visible_schemas {
|
||||
params["visible_schemas"] = serde_json::json!(visible_schemas);
|
||||
}
|
||||
if show_system_schemas {
|
||||
params["show_system_schemas"] = serde_json::Value::Bool(true);
|
||||
}
|
||||
self.call_method_with_timeout(AgentMethod::ListSchemas, params, timeout_duration).await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2504,27 +2504,49 @@ pub async fn list_object_statistics(pool: &Pool, schema: &str) -> Result<Vec<Obj
|
|||
}
|
||||
|
||||
pub async fn list_schemas(pool: &Pool) -> Result<Vec<String>, String> {
|
||||
Ok(list_schema_infos(pool).await?.into_iter().map(|schema| schema.name).collect())
|
||||
list_schemas_with_system(pool, false).await
|
||||
}
|
||||
|
||||
pub async fn list_schema_infos(pool: &Pool) -> Result<Vec<SchemaInfo>, String> {
|
||||
list_schema_infos_with_system(pool, false).await
|
||||
}
|
||||
|
||||
pub async fn list_schemas_with_system(pool: &Pool, show_system_schemas: bool) -> Result<Vec<String>, String> {
|
||||
Ok(list_schema_infos_with_system(pool, show_system_schemas).await?.into_iter().map(|schema| schema.name).collect())
|
||||
}
|
||||
|
||||
const POSTGRES_SCHEMA_INFOS_SQL: &str = "SELECT n.nspname AS schema_name, d.description AS schema_comment \
|
||||
FROM pg_catalog.pg_namespace n \
|
||||
LEFT JOIN pg_catalog.pg_description d \
|
||||
ON d.objoid = n.oid \
|
||||
AND d.objsubid = 0 \
|
||||
AND d.classoid = 'pg_namespace'::regclass \
|
||||
ORDER BY n.nspname";
|
||||
|
||||
const POSTGRES_SCHEMA_INFOS_HIDE_SYSTEM_SQL: &str = "SELECT n.nspname AS schema_name, d.description AS schema_comment \
|
||||
FROM pg_catalog.pg_namespace n \
|
||||
LEFT JOIN pg_catalog.pg_description d \
|
||||
ON d.objoid = n.oid \
|
||||
AND d.objsubid = 0 \
|
||||
AND d.classoid = 'pg_namespace'::regclass \
|
||||
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') \
|
||||
AND n.nspname NOT LIKE 'pg_toast_temp_%' \
|
||||
AND n.nspname NOT LIKE 'pg_temp_%' \
|
||||
ORDER BY n.nspname";
|
||||
|
||||
fn postgres_schema_infos_sql(show_system_schemas: bool) -> &'static str {
|
||||
if show_system_schemas {
|
||||
POSTGRES_SCHEMA_INFOS_SQL
|
||||
} else {
|
||||
POSTGRES_SCHEMA_INFOS_HIDE_SYSTEM_SQL
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_schema_infos_with_system(pool: &Pool, show_system_schemas: bool) -> Result<Vec<SchemaInfo>, String> {
|
||||
let client = checkout_postgres_client(pool, None, super::connection_timeout()).await?;
|
||||
let rows = postgres_query_cached(
|
||||
&client,
|
||||
"SELECT n.nspname AS schema_name, d.description AS schema_comment \
|
||||
FROM pg_catalog.pg_namespace n \
|
||||
LEFT JOIN pg_catalog.pg_description d \
|
||||
ON d.objoid = n.oid \
|
||||
AND d.objsubid = 0 \
|
||||
AND d.classoid = 'pg_namespace'::regclass \
|
||||
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') \
|
||||
AND n.nspname NOT LIKE 'pg_toast_temp_%' \
|
||||
AND n.nspname NOT LIKE 'pg_temp_%' \
|
||||
ORDER BY n.nspname",
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = postgres_query_cached(&client, postgres_schema_infos_sql(show_system_schemas), &[])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(rows
|
||||
.iter()
|
||||
|
|
@ -5719,4 +5741,15 @@ mod tests {
|
|||
assert!(postgres_completion_columns_sql().contains("a.attname ILIKE $3 ESCAPE '~'"));
|
||||
assert!(postgres_visible_table_schema_sql().contains("pg_catalog.pg_table_is_visible(c.oid)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_schema_info_sql_only_filters_system_schemas_when_disabled() {
|
||||
let hidden_sql = postgres_schema_infos_sql(false);
|
||||
assert!(hidden_sql.contains("information_schema"));
|
||||
assert!(hidden_sql.contains("pg_temp_%"));
|
||||
|
||||
let visible_sql = postgres_schema_infos_sql(true);
|
||||
assert!(!visible_sql.contains("NOT IN"));
|
||||
assert!(!visible_sql.contains("NOT LIKE"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4643,6 +4643,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ pub struct ConnectionConfig {
|
|||
pub visible_databases: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub visible_schemas: Option<HashMap<String, Vec<String>>>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub show_system_schemas: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub attached_databases: Vec<AttachedDatabaseConfig>,
|
||||
/// SQL statements executed right after the connection is established
|
||||
|
|
@ -561,6 +563,8 @@ struct ConnectionConfigData {
|
|||
#[serde(default)]
|
||||
pub visible_schemas: Option<HashMap<String, Vec<String>>>,
|
||||
#[serde(default)]
|
||||
pub show_system_schemas: bool,
|
||||
#[serde(default)]
|
||||
pub attached_databases: Vec<AttachedDatabaseConfig>,
|
||||
#[serde(default)]
|
||||
pub init_script: Option<String>,
|
||||
|
|
@ -650,6 +654,7 @@ impl From<ConnectionConfigData> for ConnectionConfig {
|
|||
database: data.database,
|
||||
visible_databases: data.visible_databases,
|
||||
visible_schemas: data.visible_schemas,
|
||||
show_system_schemas: data.show_system_schemas,
|
||||
attached_databases: data.attached_databases,
|
||||
init_script: data.init_script,
|
||||
color: data.color,
|
||||
|
|
@ -2197,6 +2202,7 @@ mod tests {
|
|||
database: database.map(str::to_string),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -870,6 +870,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
@ -301,6 +302,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: vec![],
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -3662,6 +3662,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
@ -4786,6 +4787,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -1180,10 +1180,12 @@ async fn list_schema_infos_once(
|
|||
database: &str,
|
||||
) -> Result<Vec<db::SchemaInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
let show_system_schemas = db_config.as_ref().is_some_and(|config| config.show_system_schemas);
|
||||
{
|
||||
let connections = state.connections.read().await;
|
||||
if let Some(PoolKind::Postgres(pool)) = connections.get(&pool_key) {
|
||||
return db::postgres::list_schema_infos(pool).await;
|
||||
return db::postgres::list_schema_infos_with_system(pool, show_system_schemas).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1250,6 +1252,7 @@ async fn list_schemas_once(
|
|||
) -> Result<Vec<String>, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
let show_system_schemas = db_config.as_ref().is_some_and(|config| config.show_system_schemas);
|
||||
let visible_schema_filter = visible_schema_filter(db_config.as_ref(), database, apply_visible_filter);
|
||||
|
||||
{
|
||||
|
|
@ -1275,6 +1278,7 @@ async fn list_schemas_once(
|
|||
.list_schemas_filtered::<Vec<String>>(
|
||||
database,
|
||||
visible_schema_filter.as_deref(),
|
||||
show_system_schemas,
|
||||
agent_metadata_timeout(db_config.as_ref()),
|
||||
)
|
||||
.await
|
||||
|
|
@ -1286,9 +1290,9 @@ async fn list_schemas_once(
|
|||
if let Some(config) = fallback_config.as_ref() {
|
||||
match native_postgres_metadata_pool(state, connection_id, database, config).await {
|
||||
Ok(Some(pool)) => {
|
||||
return db::postgres::list_schemas(&pool).await.map(|schemas| {
|
||||
filter_visible_schema_names(schemas, visible_schema_filter.as_deref())
|
||||
})
|
||||
return db::postgres::list_schemas_with_system(&pool, show_system_schemas).await.map(
|
||||
|schemas| filter_visible_schema_names(schemas, visible_schema_filter.as_deref()),
|
||||
)
|
||||
}
|
||||
Ok(None) => {
|
||||
return Ok(filter_visible_schema_names(schemas, visible_schema_filter.as_deref()))
|
||||
|
|
@ -1310,7 +1314,7 @@ async fn list_schemas_once(
|
|||
if let Some(pool) =
|
||||
native_postgres_metadata_pool(state, connection_id, database, config).await?
|
||||
{
|
||||
return db::postgres::list_schemas(&pool)
|
||||
return db::postgres::list_schemas_with_system(&pool, show_system_schemas)
|
||||
.await
|
||||
.map(|schemas| filter_visible_schema_names(schemas, visible_schema_filter.as_deref()))
|
||||
.map_err(|fallback_error| {
|
||||
|
|
@ -1333,7 +1337,7 @@ async fn list_schemas_once(
|
|||
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)
|
||||
PoolKind::Postgres(p) => db::postgres::list_schemas_with_system(p, show_system_schemas)
|
||||
.await
|
||||
.map(|schemas| filter_visible_schema_names(schemas, visible_schema_filter.as_deref())),
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
|
|
@ -3034,6 +3038,7 @@ mod tests {
|
|||
database: Some("demo".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ mod tests {
|
|||
database: Some("demo".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -3791,6 +3791,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
@ -3853,6 +3854,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -4936,6 +4936,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ fn postgres_test_config(id: &str, port: u16) -> ConnectionConfig {
|
|||
read_only: false,
|
||||
is_production: false,
|
||||
production_databases: vec![],
|
||||
show_system_schemas: false,
|
||||
database_info: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ fn live_postgres_config(
|
|||
read_only: false,
|
||||
is_production: false,
|
||||
production_databases: vec![],
|
||||
show_system_schemas: false,
|
||||
database_info: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
|
|||
read_only: false,
|
||||
is_production: false,
|
||||
production_databases: vec![],
|
||||
show_system_schemas: false,
|
||||
database_info: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
|
|||
read_only: false,
|
||||
is_production: false,
|
||||
production_databases: vec![],
|
||||
show_system_schemas: false,
|
||||
database_info: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ fn live_sqlserver_config(id: &str, database: &str) -> dbx_core::models::connecti
|
|||
read_only: false,
|
||||
is_production: false,
|
||||
production_databases: vec![],
|
||||
show_system_schemas: false,
|
||||
database_info: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ mod tests {
|
|||
database: None,
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ mod tests {
|
|||
database: Some("RestCloud_V45PUB_Gateway".to_string()),
|
||||
visible_databases: None,
|
||||
visible_schemas: None,
|
||||
show_system_schemas: false,
|
||||
attached_databases: Vec::new(),
|
||||
init_script: None,
|
||||
color: None,
|
||||
|
|
|
|||
Loading…
Reference in New Issue