fix(dameng): support configurable system schemas

This commit is contained in:
zipg 2026-07-22 17:40:40 +08:00 committed by GitHub
parent 697b7a4b80
commit a81d6da604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 146 additions and 66 deletions

View File

@ -72,11 +72,6 @@ public final class DamengAgent extends BaseDatabaseAgent {
ON schema_object.OBJECT_ID = m.SCHID AND schema_object.OBJECT_TYPE = 'SCH'
) mv ON mv.OWNER = o.OWNER AND mv.MVIEW_NAME = o.OBJECT_NAME
""".stripIndent().trim();
private static final Set<String> SYSTEM_USERS = Set.of(
"SYS", "SYSAUDITOR", "SYSSSO", "CTISYS",
"SYSDBA", "SYS_DBA", "_SYS_STATISTICS", "SYS_PHM"
);
private Connection connection;
private String connectedUsername;
@ -157,13 +152,8 @@ public final class DamengAgent extends BaseDatabaseAgent {
private List<String> listVisibleUsers() throws Exception {
List<String> result = new ArrayList<>();
String placeholders = String.join(",", SYSTEM_USERS.stream().map(user -> "?").toList());
String sql = "SELECT USERNAME FROM ALL_USERS WHERE USERNAME NOT IN (" + placeholders + ") ORDER BY USERNAME";
String sql = "SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
int index = 1;
for (String user : SYSTEM_USERS) {
stmt.setString(index++, user);
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(1));
@ -175,13 +165,8 @@ public final class DamengAgent extends BaseDatabaseAgent {
private List<String> listVisibleSchemas() throws Exception {
List<String> result = new ArrayList<>();
String placeholders = String.join(",", SYSTEM_USERS.stream().map(user -> "?").toList());
String sql = "SELECT NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND NAME NOT IN (" + placeholders + ") ORDER BY NAME";
String sql = "SELECT NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' ORDER BY NAME";
try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) {
int index = 1;
for (String user : SYSTEM_USERS) {
stmt.setString(index++, user);
}
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
result.add(rs.getString(1));

View File

@ -106,12 +106,24 @@ class DamengAgentMetadataTest {
List<String> schemas = agent.listSchemas();
Assertions.assertEquals(List.of("APP", "EMPTY_SCHEMA"), schemas);
Assertions.assertEquals(List.of("APP", "EMPTY_SCHEMA", "SYSDBA"), schemas);
Assertions.assertTrue(sqls.stream().anyMatch(sql -> sql.contains("SYS.SYSOBJECTS") && sql.contains("TYPE$ = 'SCH'")), String.join("\n", sqls));
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("ALL_USERS")), String.join("\n", sqls));
Assertions.assertTrue(sqls.stream().noneMatch(sql -> sql.contains("ALL_OBJECTS")), String.join("\n", sqls));
}
@Test
void listSchemasLeavesSysdbaAvailableForFrontendFiltering() {
DamengAgent agent = new DamengAgent();
List<String> params = new ArrayList<>();
TestSupport.setPrivateConnection(agent, schemaConnection(params));
List<String> schemas = agent.listSchemas();
Assertions.assertEquals(List.of("APP", "SYSDBA"), schemas);
Assertions.assertTrue(params.isEmpty(), params.toString());
}
@Test
void listSchemasFallsBackToAllUsersWithoutSysObjectsPrivilege() {
DamengAgent agent = new DamengAgent();
@ -120,7 +132,7 @@ class DamengAgentMetadataTest {
List<String> schemas = agent.listSchemas();
Assertions.assertEquals(List.of("APP", "REPORTING"), schemas);
Assertions.assertEquals(List.of("APP", "REPORTING", "SYSDBA"), schemas);
Assertions.assertEquals(2, sqls.size(), String.join("\n", sqls));
Assertions.assertTrue(sqls.get(0).contains("SYS.SYSOBJECTS"), sqls.get(0));
Assertions.assertTrue(sqls.get(1).contains("ALL_USERS"), sqls.get(1));
@ -522,7 +534,7 @@ class DamengAgentMetadataTest {
return dbmsMetadataStatement(dbmsMetadataDdl, dbmsMetadataResultOpen);
}
if (sql.contains("SYS.SYSOBJECTS") && sql.contains("TYPE$ = 'SCH'")) {
return metadataStatement(List.of(List.of("APP"), List.of("EMPTY_SCHEMA")));
return metadataStatement(List.of(List.of("APP"), List.of("EMPTY_SCHEMA"), List.of("SYSDBA")));
}
if (sql.contains("ALL_CONS_COLUMNS")) {
return metadataStatement(List.of(List.of("ID")));
@ -720,7 +732,7 @@ class DamengAgentMetadataTest {
return failingMetadataStatement("no SYS.SYSOBJECTS privilege");
}
if (sql.contains("ALL_USERS")) {
return metadataStatement(List.of(List.of("APP"), List.of("REPORTING")));
return metadataStatement(List.of(List.of("APP"), List.of("REPORTING"), List.of("SYSDBA")));
}
throw new AssertionError("Unexpected SQL: " + sql);
}
@ -734,6 +746,22 @@ class DamengAgentMetadataTest {
});
}
private static Connection schemaConnection(List<String> params) {
return proxy(Connection.class, (method, args) -> {
String name = method.getName();
if ("prepareStatement".equals(name)) {
return metadataStatement(List.of(List.of("APP"), List.of("SYSDBA")), params);
}
if ("close".equals(name)) {
return null;
}
if ("isClosed".equals(name)) {
return false;
}
return defaultValue(method.getReturnType());
});
}
private static PreparedStatement failingMetadataStatement(String message) {
return proxy(PreparedStatement.class, (method, args) -> {
if ("executeQuery".equals(method.getName())) {

View File

@ -80,8 +80,8 @@ import {
Square,
Trash2,
} from "@lucide/vue";
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connection/connectionVisibleDatabases";
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleObjectFiltersNeedReset } from "@/lib/connection/connectionVisibleDatabases";
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
import VisibleSchemasDialog from "@/components/sidebar/VisibleSchemasDialog.vue";
import CloudflareD1ConnectionFields from "@/components/connection/CloudflareD1ConnectionFields.vue";
@ -1745,6 +1745,7 @@ watch(
is_production: config.is_production || false,
production_databases: config.production_databases || [],
visible_databases: config.visible_databases,
visible_schemas: config.visible_schemas,
};
productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0;
connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : "";
@ -2240,12 +2241,12 @@ const visibleDatabaseSummary = computed(() => {
if (!Array.isArray(configured)) return t("visibleDatabases.showAll");
return t("visibleDatabases.selectedCount", { selected: configured.length, total: visibleDatabaseNames.value.length });
});
const listedVisibleDatabaseNames = computed(() => {
if (visibleFilterUsesSchemas.value) return visibleDatabaseNames.value;
const defaultListedVisibleDatabaseNames = computed(() => {
const connection = connectionConfigSnapshotForVisibleDatabases();
if (visibleDatabaseShowSystem.value) return visibleDatabaseNames.value;
if (visibleFilterUsesSchemas.value) return filterSchemaNamesForVisiblePicker(visibleDatabaseNames.value, connection);
return filterDatabaseNamesForVisiblePicker(visibleDatabaseNames.value, connection);
});
const listedVisibleDatabaseNames = computed(() => (visibleDatabaseShowSystem.value ? visibleDatabaseNames.value : defaultListedVisibleDatabaseNames.value));
const filteredVisibleDatabaseNames = computed(() => {
const query = visibleDatabaseSearchText.value.trim().toLowerCase();
if (!query) return listedVisibleDatabaseNames.value;
@ -2254,11 +2255,8 @@ const filteredVisibleDatabaseNames = computed(() => {
const visibleDatabaseSelectedCount = computed(() => visibleDatabaseSelection.value.size);
const visibleDatabaseTotalCount = computed(() => listedVisibleDatabaseNames.value.length);
const visibleDatabaseCanSave = computed(() => canSaveVisibleDatabaseSelection([...visibleDatabaseSelection.value]));
const visibleDatabaseHasSystemDatabases = computed(() => {
if (visibleFilterUsesSchemas.value) return false;
const connection = connectionConfigSnapshotForVisibleDatabases();
return visibleDatabaseNames.value.some((database) => isSystemDatabaseName(connection.db_type, database));
});
const visibleDatabaseHasSystemObjects = computed(() => defaultListedVisibleDatabaseNames.value.length < visibleDatabaseNames.value.length);
const visibleSystemObjectsLabelKey = computed(() => (visibleFilterUsesSchemas.value ? "visibleSchemas.showSystemSchemas" : "visibleDatabases.showSystemDatabases"));
const filteredProductionDatabaseNames = computed(() => {
const query = productionDatabaseSearchText.value.trim().toLowerCase();
if (!query) return productionDatabaseNames.value;
@ -3227,10 +3225,12 @@ async function openVisibleDatabasesPicker() {
await api.connectDb(draftConfig);
const names = await loadVisibleDatabaseNames(draftId, draftConfig);
visibleDatabaseNames.value = names;
visibleDatabaseShowSystem.value = false;
const configuredSchemas = visibleSchemaObjectSelection.value;
const initialSelection = visibleFilterUsesSchemas.value ? (Array.isArray(configuredSchemas) ? normalizeVisibleSchemaSelection(configuredSchemas, names) : names) : initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig);
const initialSelection = visibleFilterUsesSchemas.value ? (Array.isArray(configuredSchemas) ? normalizeVisibleSchemaSelection(configuredSchemas, names) : filterSchemaNamesForVisiblePicker(names, draftConfig)) : initialVisibleDatabaseSelection(names, form.value.visible_databases, draftConfig);
visibleDatabaseSelection.value = new Set(initialSelection);
visibleDatabaseShowSystem.value = !visibleFilterUsesSchemas.value && initialSelection.some((database) => isSystemDatabaseName(draftConfig.db_type, database));
const defaultVisible = new Set(defaultListedVisibleDatabaseNames.value);
visibleDatabaseShowSystem.value = initialSelection.some((name) => !defaultVisible.has(name));
showVisibleDatabasesDialog.value = true;
} catch (e: any) {
visibleDatabaseNames.value = [];
@ -3623,18 +3623,19 @@ watch([() => form.value.db_type, () => form.value.username], () => {
watch(
() => connectionConfigSnapshotForVisibleDatabases(),
(current, previous) => {
if (!previous || !form.value.visible_databases?.length) return;
if (!visibleDatabaseSelectionIsStale(previous, current)) return;
if (!previous || !visibleObjectFiltersNeedReset(previous, current)) return;
form.value.visible_databases = undefined;
visibleDatabaseNames.value = [];
visibleDatabaseSelection.value = new Set();
form.value.visible_schemas = undefined;
resetVisibleDatabaseDraftState();
resetVisibleSchemasState();
},
);
watch(visibleDatabaseShowSystem, (show) => {
if (show) return;
const connection = connectionConfigSnapshotForVisibleDatabases();
visibleDatabaseSelection.value = new Set([...visibleDatabaseSelection.value].filter((database) => !isSystemDatabaseName(connection.db_type, database)));
const visible = new Set(visibleFilterUsesSchemas.value ? filterSchemaNamesForVisiblePicker(visibleDatabaseNames.value, connection) : filterDatabaseNamesForVisiblePicker(visibleDatabaseNames.value, connection));
visibleDatabaseSelection.value = new Set([...visibleDatabaseSelection.value].filter((name) => visible.has(name)));
});
watch(canUseTransportLayers, (value) => {
@ -6250,9 +6251,9 @@ function openExternalUrl(url: string) {
{{ t(visibleObjectEmptySelectionKey) }}
</p>
<label v-if="visibleDatabaseHasSystemDatabases" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
<label v-if="visibleDatabaseHasSystemObjects" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
<input v-model="visibleDatabaseShowSystem" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoadingVisibleDatabases || !!visibleDatabaseError" />
<span>{{ t("visibleDatabases.showSystemDatabases") }}</span>
<span>{{ t(visibleSystemObjectsLabelKey) }}</span>
</label>
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
@ -6364,6 +6365,8 @@ function openExternalUrl(url: string) {
:connection-id="''"
:connection-name="form.name || selectedProfile().label"
:database="visibleSchemasDatabaseKey"
:database-type="form.db_type"
:username="form.username"
:draft-schema-names="visibleSchemaNames"
:draft-initial-selection="visibleSchemaInitialSelection"
:draft-loading="isLoadingVisibleSchemas"

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, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, isSystemDatabaseName, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForVisiblePicker, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
import * as api from "@/lib/backend/api";
const props = defineProps<{
@ -40,11 +40,12 @@ const descriptionKey = computed(() => (isSchemaFilterMode.value ? "visibleSchema
const searchPlaceholderKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.searchPlaceholder" : "visibleDatabases.searchPlaceholder"));
const emptySelectionKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.emptySelection" : "visibleDatabases.emptySelection"));
const loadFailedKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.loadFailed" : "visibleDatabases.loadFailed"));
const listedObjectNames = computed(() => {
if (isSchemaFilterMode.value) return objectNames.value;
if (showSystemDatabases.value) return objectNames.value;
const showSystemObjectsKey = computed(() => (isSchemaFilterMode.value ? "visibleSchemas.showSystemSchemas" : "visibleDatabases.showSystemDatabases"));
const defaultObjectNames = computed(() => {
if (isSchemaFilterMode.value) return filterSchemaNamesForVisiblePicker(objectNames.value, connection.value);
return filterDatabaseNamesForVisiblePicker(objectNames.value, connection.value);
});
const listedObjectNames = computed(() => (showSystemDatabases.value ? objectNames.value : defaultObjectNames.value));
const filteredObjectNames = computed(() => {
const query = searchText.value.trim().toLowerCase();
if (!query) return listedObjectNames.value;
@ -53,7 +54,7 @@ const filteredObjectNames = computed(() => {
const selectedCount = computed(() => selectedNames.value.size);
const totalCount = computed(() => listedObjectNames.value.length);
const canSaveSelection = computed(() => canSaveVisibleDatabaseSelection([...selectedNames.value]));
const hasSystemDatabases = computed(() => !isSchemaFilterMode.value && objectNames.value.some((database) => isSystemDatabaseName(connection.value?.db_type, database)));
const hasSystemObjects = computed(() => defaultObjectNames.value.length < objectNames.value.length);
const showAllDisabled = computed(() => {
if (isSchemaFilterMode.value) {
return !connection.value?.visible_schemas?.[databaseKey.value];
@ -73,7 +74,8 @@ watch(
watch(showSystemDatabases, (show) => {
if (show) return;
selectedNames.value = new Set([...selectedNames.value].filter((database) => !isSystemDatabaseName(connection.value?.db_type, database)));
const visible = new Set(listedObjectNames.value);
selectedNames.value = new Set([...selectedNames.value].filter((name) => visible.has(name)));
});
async function loadDatabases() {
@ -83,10 +85,12 @@ async function loadDatabases() {
try {
const names = await loadObjectNames();
objectNames.value = names;
showSystemDatabases.value = false;
const configured = isSchemaFilterMode.value ? connection.value?.visible_schemas?.[databaseKey.value] : connection.value?.visible_databases;
const initialSelection = Array.isArray(configured) ? normalizeVisibleDatabaseSelection(configured, names) : listedObjectNames.value;
selectedNames.value = new Set(initialSelection);
showSystemDatabases.value = !isSchemaFilterMode.value && initialSelection.some((database) => isSystemDatabaseName(connection.value?.db_type, database));
const defaultVisible = new Set(defaultObjectNames.value);
showSystemDatabases.value = initialSelection.some((name) => !defaultVisible.has(name));
} catch (e: any) {
objectNames.value = [];
selectedNames.value = new Set();
@ -190,9 +194,9 @@ async function saveSelection() {
{{ t(emptySelectionKey) }}
</p>
<label v-if="hasSystemDatabases" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
<label v-if="hasSystemObjects" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
<input v-model="showSystemDatabases" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoading || !!errorMessage" />
<span>{{ t("visibleDatabases.showSystemDatabases") }}</span>
<span>{{ t(showSystemObjectsKey) }}</span>
</label>
<div class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">

View File

@ -7,7 +7,8 @@ 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 { normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
import { filterSchemaNamesForVisiblePicker, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
import type { DatabaseType } from "@/types/database";
import * as api from "@/lib/backend/api";
import { addVisibleSchemas, initialVisibleSchemaSelection, resolveVisibleSchemaSaveAction, selectVisibleSchemas } from "./visibleSchemasDialogState";
@ -16,6 +17,8 @@ const props = defineProps<{
connectionId: string;
connectionName: string;
database: string;
databaseType?: DatabaseType;
username?: string;
/** Draft mode: parent provides schemas/loading/error externally. Component emits events instead of calling store. */
draftMode?: boolean;
draftSchemaNames?: string[];
@ -39,22 +42,26 @@ const searchText = ref("");
const isLoading = ref(false);
const isSaving = ref(false);
const errorMessage = ref("");
const showSystemSchemas = ref(false);
const isDraftMode = computed(() => props.draftMode);
const isLoadingState = computed(() => (isDraftMode.value ? props.draftLoading : isLoading.value));
const errorMessageState = computed(() => (isDraftMode.value ? props.draftError || "" : errorMessage.value));
const connection = computed(() => (isDraftMode.value ? { db_type: props.databaseType, username: props.username || "" } : connectionStore.getConfig(props.connectionId)));
const allSchemaNames = computed(() => (isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value));
const listedSchemaNames = computed(() => (showSystemSchemas.value ? allSchemaNames.value : filterSchemaNamesForVisiblePicker(allSchemaNames.value, connection.value)));
const hasSystemSchemas = computed(() => filterSchemaNamesForVisiblePicker(allSchemaNames.value, connection.value).length < allSchemaNames.value.length);
const filteredSchemaNames = computed(() => {
const query = searchText.value.trim().toLowerCase();
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
const names = listedSchemaNames.value;
if (!query) return names;
return names.filter((name) => name.toLowerCase().includes(query));
});
const filteredSchemaItems = computed(() => filteredSchemaNames.value.map((name) => ({ name })));
const selectedCount = computed(() => selectedNames.value.size);
const totalCount = computed(() => {
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
return names.length;
return listedSchemaNames.value.length;
});
const canSaveSelection = computed(() => selectedNames.value.size > 0);
@ -75,8 +82,7 @@ watch(
function initDraftMode() {
searchText.value = "";
const names = props.draftSchemaNames || [];
// Draft callers historically use an empty array for both absent and all-selected state.
selectedNames.value = initialVisibleSchemaSelection(names, props.draftInitialSelection, true);
initializeSelection(names, props.draftInitialSelection, true);
}
async function loadSchemas() {
@ -90,7 +96,7 @@ async function loadSchemas() {
const names = await api.listSchemas(props.connectionId, database);
schemaNames.value = names;
const configured = config?.visible_schemas?.[database || ""];
selectedNames.value = initialVisibleSchemaSelection(names, configured);
initializeSelection(names, configured);
} catch (e: any) {
schemaNames.value = [];
selectedNames.value = new Set();
@ -100,6 +106,21 @@ async function loadSchemas() {
}
}
function initializeSelection(names: string[], configured: string[] | undefined, emptySelectionMeansAll = false) {
showSystemSchemas.value = false;
const hasExplicitSelection = configured !== undefined && !(emptySelectionMeansAll && configured.length === 0);
const initial = hasExplicitSelection ? initialVisibleSchemaSelection(names, configured) : selectVisibleSchemas(filterSchemaNamesForVisiblePicker(names, connection.value));
const defaultVisible = new Set(filterSchemaNamesForVisiblePicker(names, connection.value));
selectedNames.value = initial;
showSystemSchemas.value = hasExplicitSelection && [...initial].some((name) => !defaultVisible.has(name));
}
watch(showSystemSchemas, (show) => {
if (show) return;
const visible = new Set(listedSchemaNames.value);
selectedNames.value = new Set([...selectedNames.value].filter((name) => visible.has(name)));
});
function toggleSchema(schema: string) {
const next = new Set(selectedNames.value);
if (next.has(schema)) next.delete(schema);
@ -110,8 +131,7 @@ function toggleSchema(schema: string) {
const isSearching = computed(() => searchText.value.trim().length > 0);
function selectAll() {
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
selectedNames.value = selectVisibleSchemas(names);
selectedNames.value = selectVisibleSchemas(listedSchemaNames.value);
}
function selectFiltered() {
@ -148,7 +168,7 @@ async function showAllSchemas() {
async function saveSelection() {
if (!canSaveSelection.value || isSaving.value) return;
const names = isDraftMode.value ? props.draftSchemaNames || [] : schemaNames.value;
const names = listedSchemaNames.value;
const normalized = normalizeVisibleSchemaSelection([...selectedNames.value], names);
if (isDraftMode.value) {
emit("draft:save", normalized);
@ -211,6 +231,11 @@ async function saveSelection() {
{{ t("visibleSchemas.emptySelection") }}
</p>
<label v-if="hasSystemSchemas" class="flex h-8 items-center gap-2 rounded-md px-1 text-xs text-muted-foreground">
<input v-model="showSystemSchemas" type="checkbox" class="h-3.5 w-3.5 accent-primary" :disabled="isLoadingState || !!errorMessageState" />
<span>{{ t("visibleSchemas.showSystemSchemas") }}</span>
</label>
<div v-if="isLoadingState || errorMessageState || !filteredSchemaItems.length" class="h-72 overflow-y-auto rounded-md border bg-background/50 p-1">
<div v-if="isLoadingState" class="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />

View File

@ -1947,6 +1947,7 @@ export default {
selectFiltered: "Select search results",
clear: "Clear",
showAll: "Show all",
showSystemSchemas: "Show system schemas",
save: "Save",
emptySelection: "Select at least one schema, or use Show all to clear the filter.",
loadFailed: "Failed to load schemas: {message}",

View File

@ -1889,6 +1889,7 @@ export default withEnglishFallback({
selectFiltered: "Seleccionar resultados de búsqueda",
clear: "Limpiar",
showAll: "Mostrar todo",
showSystemSchemas: "Mostrar schemas del sistema",
save: "Guardar",
emptySelection: "Selecciona al menos un schema, o usa Mostrar todo para quitar el filtro.",
loadFailed: "No se pudieron cargar los schemas: {message}",

View File

@ -1887,6 +1887,7 @@ export default withEnglishFallback({
selectFiltered: "Seleziona risultati ricerca",
clear: "Cancella",
showAll: "Mostra tutto",
showSystemSchemas: "Mostra schema di sistema",
save: "Salva",
emptySelection: "Seleziona almeno uno schema, oppure usa Mostra tutto per rimuovere il filtro.",
loadFailed: "Impossibile caricare gli schema: {message}",

View File

@ -1888,6 +1888,7 @@ export default withEnglishFallback({
selectFiltered: "検索結果を選択",
clear: "クリア",
showAll: "すべて表示",
showSystemSchemas: "システムスキーマを表示",
save: "保存",
emptySelection: "少なくとも1つのスキーマを選択するか、すべて表示でフィルターをクリアしてください。",
loadFailed: "スキーマの読み込みに失敗しました: {message}",

View File

@ -1889,6 +1889,7 @@ export default withEnglishFallback({
selectFiltered: "Selecionar resultados da pesquisa",
clear: "Limpar",
showAll: "Mostrar todos",
showSystemSchemas: "Mostrar schemas do sistema",
save: "Salvar",
emptySelection: "Selecione pelo menos um schema, ou use Mostrar todos para limpar o filtro.",
loadFailed: "Falha ao carregar schemas: {message}",

View File

@ -1947,6 +1947,7 @@ export default withEnglishFallback({
selectFiltered: "选中搜索结果",
clear: "清空",
showAll: "显示全部",
showSystemSchemas: "显示系统 Schema",
save: "保存",
emptySelection: "至少选择一个 Schema或使用「显示全部」清除过滤。",
loadFailed: "加载 Schema 失败:{message}",

View File

@ -1889,6 +1889,7 @@ export default withEnglishFallback({
selectFiltered: "選取搜尋結果",
clear: "清空",
showAll: "顯示全部",
showSystemSchemas: "顯示系統 Schema",
save: "儲存",
emptySelection: "至少選擇一個 Schema或使用「顯示全部」清除篩選。",
loadFailed: "載入 Schema 失敗:{message}",

View File

@ -12,6 +12,8 @@ type VisibleDatabaseConnectionFields = Pick<
"db_type" | "driver_profile" | "host" | "port" | "username" | "database" | "connection_string" | "url_params" | "redis_connection_mode" | "redis_sentinel_master" | "redis_sentinel_nodes" | "redis_cluster_nodes" | "etcd_endpoints" | "jdbc_driver_class"
>;
type VisibleObjectFilterConnectionFields = VisibleDatabaseConnectionFields & Pick<ConnectionConfig, "visible_databases" | "visible_schemas">;
export function buildDraftVisibleDatabasesConnectionId(seed: string): string {
return `${DRAFT_VISIBLE_DATABASES_PREFIX}${seed}`;
}
@ -38,6 +40,12 @@ export function visibleDatabaseSelectionIsStale(previous: VisibleDatabaseConnect
return visibleDatabaseFingerprint(previous) !== visibleDatabaseFingerprint(current);
}
export function visibleObjectFiltersNeedReset(previous: VisibleObjectFilterConnectionFields, current: VisibleObjectFilterConnectionFields): boolean {
const hasVisibleDatabaseFilter = Array.isArray(current.visible_databases);
const hasVisibleSchemaFilter = !!current.visible_schemas && Object.keys(current.visible_schemas).length > 0;
return (hasVisibleDatabaseFilter || hasVisibleSchemaFilter) && visibleDatabaseSelectionIsStale(previous, current);
}
function visibleDatabaseFingerprint(connection: VisibleDatabaseConnectionFields): string {
return JSON.stringify({
db_type: connection.db_type,

View File

@ -51,7 +51,7 @@ const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>>
"xdb",
"xs$null",
]),
dameng: new Set(["ctisys", "dba", "sys", "sysauditor", "syssso", "system"]),
dameng: new Set(["_sys_statistics", "ctisys", "dba", "sys", "sys_dba", "sys_phm", "sysauditor", "sysdba", "sysdbo", "syssso", "system"]),
saphana: new Set(["_sys_afl", "_sys_bi", "_sys_bic", "_sys_repo", "_sys_statistics", "sys"]),
cassandra: new Set(["system", "system_auth", "system_distributed", "system_schema", "system_traces", "system_views", "system_virtual_schema"]),
neo4j: new Set(["system"]),
@ -102,6 +102,11 @@ 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[] {
const currentSchema = connection?.username?.trim().toLowerCase();
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !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";
}
@ -110,13 +115,13 @@ 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"> | undefined, database: string): string[] {
export function filterSchemaNamesForConnection(schemaNames: string[], connection: (Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases"> & Partial<Pick<ConnectionConfig, "username">>) | 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));
return filterSchemaNamesForVisiblePicker(schemaNames, connection);
}
const visible = new Set(visibleSchemas![database]);
return schemaNames.filter((name) => visible.has(name));

View File

@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { appendVisibleDatabaseSelection, buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connection/connectionVisibleDatabases.ts";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForConnection, normalizeVisibleSchemaSelection } from "../../apps/desktop/src/lib/database/visibleDatabases.ts";
import { appendVisibleDatabaseSelection, buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, visibleDatabaseSelectionIsStale, visibleObjectFiltersNeedReset, initialVisibleDatabaseSelection } from "../../apps/desktop/src/lib/connection/connectionVisibleDatabases.ts";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterDatabaseNamesForVisiblePicker, filterSchemaNamesForConnection, filterSchemaNamesForVisiblePicker, normalizeVisibleSchemaSelection } from "../../apps/desktop/src/lib/database/visibleDatabases.ts";
import type { ConnectionConfig } from "../../apps/desktop/src/types/database.ts";
function config(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
@ -103,8 +103,16 @@ test("Vastbase schema filters preserve ordinary schemas and explicit empty selec
assert.deepEqual(normalizeVisibleSchemaSelection(["app", "missing", "app", "public"], schemas), ["app", "public"]);
});
test("Dameng default SYSDBA user remains selectable", () => {
assert.deepEqual(filterDatabaseNamesForConnection(["SYS", "SYSDBA", "SYSAUDITOR"], config({ db_type: "dameng" })), ["SYSDBA"]);
test("Dameng hides system schemas by default but keeps the login schema visible", () => {
const schemas = ["APP", "SYS", "SYSDBA", "SYSDBO", "SYSAUDITOR"];
assert.deepEqual(filterSchemaNamesForVisiblePicker(schemas, config({ db_type: "dameng", username: "APP" })), ["APP"]);
assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "dameng", username: "SYSDBA" }), ""), ["APP", "SYSDBA"]);
assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "dameng", username: "SYSDBO" }), ""), ["APP", "SYSDBO"]);
});
test("Dameng explicit schema filters can keep SYSDBA visible", () => {
const connection = config({ db_type: "dameng", username: "APP", visible_schemas: { "": ["APP", "SYSDBA"] } });
assert.deepEqual(filterSchemaNamesForConnection(["APP", "SYS", "SYSDBA"], connection, ""), ["APP", "SYSDBA"]);
});
test("Oracle keeps an existing DIP user visible", () => {
@ -118,3 +126,10 @@ test("visible database selection is stale when connection target changes", () =>
assert.equal(visibleDatabaseSelectionIsStale(previous, config({ username: "readonly" })), true);
assert.equal(visibleDatabaseSelectionIsStale(previous, config({ database: "admin" })), true);
});
test("visible schema filters reset when the connection target changes", () => {
const previous = config({ host: "db.internal", visible_schemas: { "": ["APP"] } });
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db.internal", visible_schemas: { "": ["APP"] } })), false);
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db2.internal", visible_schemas: { "": ["APP"] } })), true);
assert.equal(visibleObjectFiltersNeedReset(previous, config({ host: "db2.internal" })), false);
});