fix(mysql): use database-specific catalog switching

This commit is contained in:
jischeng authored and GitHub committed 2026-07-29 20:41:09 +08:00
1 parent 9571519179
commit 2152f8cde8
31 files changed
+818 -182

No files matched your search

@@ -9,13 +9,14 @@ import TruncatedTextTooltip from "@/components/ui/TruncatedTextTooltip.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
import { catalogDatabaseOptionsKey, databaseAfterCatalogChange, normalizedQueryTabCatalog, queryCatalogSelectorVisible, selectedQueryCatalogName, useDatabaseOptions } from "@/composables/useDatabaseOptions";
import { useSchemaOptions } from "@/composables/useSchemaOptions";
import { connectionIconType } from "@/lib/connection/connectionPresentation";
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/database/defaultDatabase";
import { connectionDisplayName } from "@/lib/tabs/tabPresentation";
import { useConnectionGroupLabel } from "@/composables/useConnectionGroupLabel";
import { isSingleDatabase, supportsClearableQuerySchema, supportsSqlInListPaste, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
import { connectionIsDorisFamilyCatalogCapable } from "@/lib/database/databaseFeatureSupport";
import { hexToRgba } from "@/lib/common/color";
import { productionContextForDatabase } from "@/lib/database/productionSafety";
import type { QueryTab, ConnectionConfig } from "@/types/database";
@@ -46,6 +47,7 @@ const emit = defineEmits<{
importResultArchive: [];
pasteSqlInCondition: [];
changeConnection: [connectionId: string];
changeCatalog: [catalog: string | undefined, database: string];
changeDatabase: [database: string];
changeSchema: [schema: string | undefined];
setDefaultDatabase: [];
@@ -59,13 +61,30 @@ const emit = defineEmits<{
const { t } = useI18n();
const connectionStore = useConnectionStore();
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions } = useDatabaseOptions();
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
const activeCatalogs = computed(() => {
const connection = props.activeConnection;
return connection ? (catalogOptions.value[connection.id] ?? []) : [];
});
const activeCatalogNames = computed(() => activeCatalogs.value.map((catalog) => catalog.name));
const showCatalogSelector = computed(() => connectionIsDorisFamilyCatalogCapable(props.activeConnection) && queryCatalogSelectorVisible(activeCatalogs.value));
const activeCatalogValue = computed(() => selectedQueryCatalogName(activeCatalogs.value, props.activeTab.catalog));
const activeCatalogDatabaseKey = computed(() => (props.activeConnection && props.activeTab.catalog ? catalogDatabaseOptionsKey(props.activeConnection.id, props.activeTab.catalog) : ""));
const activeDatabaseOptions = computed(() => {
const connection = props.activeConnection;
return connection ? (databaseOptions.value[connection.id] ?? []) : [];
if (!connection) return [];
if (props.activeTab.catalog) return catalogDatabaseOptions.value[activeCatalogDatabaseKey.value] ?? [];
return databaseOptions.value[connection.id] ?? [];
});
const loadingActiveDatabaseOptions = computed(() => {
const connection = props.activeConnection;
if (!connection) return false;
if (props.activeTab.catalog) return loadingCatalogDatabaseOptions.value[activeCatalogDatabaseKey.value] ?? false;
return loadingDatabaseOptions.value[connection.id] ?? false;
});
const switchingCatalog = ref(false);
const connectionOptionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
const { connectionGroupLabel } = useConnectionGroupLabel();
@@ -135,6 +154,18 @@ watchEffect(() => {
loadSchemaOptions(connection.id, schemaDatabaseKey.value).catch(() => {});
}
});
watchEffect(() => {
const connection = props.activeConnection;
if (!connection || !connectionIsDorisFamilyCatalogCapable(connection)) return;
void loadCatalogOptions(connection.id).catch(() => {});
});
watchEffect(() => {
const connection = props.activeConnection;
const catalog = props.activeTab.catalog;
if (!connection || !catalog) return;
void loadCatalogDatabaseOptions(connection.id, catalog).catch(() => {});
});
const isActiveDatabaseDefault = computed(() => isDefaultDatabase(props.activeConnection, activeDatabaseValue.value));
const toolbarStyle = computed(() => {
@@ -161,6 +192,18 @@ function databaseOptionIsProduction(database: string): boolean {
if (!database || props.activeConnection?.is_production) return false;
return productionContextForDatabase(props.activeConnection, database).reason === "database";
}
async function changeCatalog(selectedCatalog: string) {
const connection = props.activeConnection;
if (!connection) return;
switchingCatalog.value = true;
try {
const catalog = normalizedQueryTabCatalog(activeCatalogs.value, selectedCatalog);
const databases = catalog ? await loadCatalogDatabaseOptions(connection.id, selectedCatalog) : await loadDatabaseOptions(connection.id).then(() => databaseOptions.value[connection.id] ?? []);
emit("changeCatalog", catalog, databaseAfterCatalogChange(props.activeTab.database, databases));
} finally {
switchingCatalog.value = false;
}
}
</script>
<template>
@@ -364,6 +407,31 @@ function databaseOptionIsProduction(database: string): boolean {
</template>
</SearchableSelect>
</div>
<div v-if="showCatalogSelector" class="flex items-center gap-1">
<SearchableSelect
:model-value="activeCatalogValue"
:options="activeCatalogNames"
:placeholder="t('editor.selectCatalog')"
:search-placeholder="t('editor.searchCatalog')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
:loading="loadingCatalogOptions[activeConnection?.id || ''] || switchingCatalog"
trigger-variant="ghost"
trigger-class="gap-1.5"
trigger-icon-class="h-3 w-3"
@update:model-value="changeCatalog"
@update:open="
(open: boolean) => {
if (open && activeConnection) loadCatalogOptions(activeConnection.id).catch(() => {});
}
"
>
<template #trigger-label="{ label, loading }">
<Layers class="h-3.5 w-3.5 shrink-0" />
<span class="truncate">{{ loading ? t("common.loading") : label }}</span>
</template>
</SearchableSelect>
</div>
<div
v-if="activeConnection?.db_type !== 'elasticsearch' && activeConnection?.db_type !== 'qdrant' && activeConnection?.db_type !== 'milvus' && activeConnection?.db_type !== 'weaviate' && activeConnection?.db_type !== 'chromadb' && activeConnection?.db_type !== 'zookeeper' && !isSingleDb"
class="flex items-center gap-1"
@@ -376,7 +444,7 @@ function databaseOptionIsProduction(database: string): boolean {
:search-placeholder="t('editor.searchDatabase')"
:empty-text="t('grid.noSearchResults')"
:loading-text="t('common.loading')"
:loading="loadingDatabaseOptions[activeConnection?.id || '']"
:loading="loadingActiveDatabaseOptions"
:display-name="databaseDisplayName"
trigger-variant="ghost"
trigger-class="gap-1.5"
@@ -384,7 +452,9 @@ function databaseOptionIsProduction(database: string): boolean {
@update:model-value="(database) => emit('changeDatabase', database)"
@update:open="
(open: boolean) => {
if (open && activeConnection) loadDatabaseOptions(activeConnection.id).catch(() => {});
if (!open || !activeConnection) return;
if (activeTab.catalog) loadCatalogDatabaseOptions(activeConnection.id, activeTab.catalog).catch(() => {});
else loadDatabaseOptions(activeConnection.id).catch(() => {});
}
"
>
@@ -408,7 +478,7 @@ function databaseOptionIsProduction(database: string): boolean {
</TooltipTrigger>
<TooltipContent>{{ t("editor.clearDatabase") }}</TooltipContent>
</Tooltip>
<Button v-if="activeDatabaseValue" variant="ghost" size="sm" class="h-6 px-2 text-[11px]" @click="isActiveDatabaseDefault ? emit('clearDefaultDatabase') : emit('setDefaultDatabase')">
<Button v-if="activeDatabaseValue && !activeTab.catalog" variant="ghost" size="sm" class="h-6 px-2 text-[11px]" @click="isActiveDatabaseDefault ? emit('clearDefaultDatabase') : emit('setDefaultDatabase')">
<Check v-if="isActiveDatabaseDefault" class="h-3 w-3" />
{{ isActiveDatabaseDefault ? t("editor.defaultDatabase") : t("editor.setDefaultDatabase") }}
</Button>
@@ -1142,13 +1142,16 @@ async function openSource(row: ObjectBrowserRow) {
}
async function openNewQuery(row: ObjectBrowserRow) {
const tabId = queryStore.createTab(props.connection.id, props.database, row.name);
const schema = row.schema || selectedSchema.value;
const tabId = queryStore.createTab(props.connection.id, props.database, row.name, "query", schema, undefined, props.catalog);
queryStore.updateSql(
tabId,
await buildTableSelectSql({
databaseType: effectiveDatabaseType.value,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connection.id),
schema: row.schema || selectedSchema.value,
catalog: props.catalog,
database: props.database,
schema,
tableName: row.name,
limit: 100,
}),
@@ -1165,7 +1168,7 @@ function openProcedureExecutionSql(sql: string) {
const row = procedureExecutionTarget.value;
if (!row || !sql) return;
const schema = row.schema || selectedSchema.value;
const tabId = queryStore.createTab(props.connection.id, props.database, `Execute - ${row.name}`, "query", schema);
const tabId = queryStore.createTab(props.connection.id, props.database, `Execute - ${row.name}`, "query", schema, undefined, props.catalog);
queryStore.updateSql(tabId, sql);
}
@@ -1173,7 +1176,7 @@ async function executeProcedureSql(sql: string) {
const row = procedureExecutionTarget.value;
if (!row || !sql) return;
const schema = row.schema || selectedSchema.value;
const tabId = queryStore.createTab(props.connection.id, props.database, `Execute - ${row.name}`, "query", schema);
const tabId = queryStore.createTab(props.connection.id, props.database, `Execute - ${row.name}`, "query", schema, undefined, props.catalog);
queryStore.updateSql(tabId, sql);
await queryStore.executeTabSql(tabId, sql);
}
@@ -1316,14 +1316,14 @@ function clearSidebarTableNameFilters() {
function openSidebarProcedureSql(sql: string) {
const target = sidebarProcedureTarget.value;
if (!target?.connectionId || !target.database || !sql) return;
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema);
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema, undefined, target.catalog);
queryStore.updateSql(tabId, sql);
}
async function executeSidebarProcedureSql(sql: string) {
const target = sidebarProcedureTarget.value;
if (!target?.connectionId || !target.database || !sql) return;
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema);
const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema, undefined, target.catalog);
queryStore.updateSql(tabId, sql);
await queryStore.executeTabSql(tabId, sql);
}
@@ -1186,7 +1186,7 @@ async function loadTemplateContext(allowView = false) {
let columns: ColumnInfo[] = [];
try {
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label, node.catalog);
} catch (e) {
console.warn("[DBX][tableSqlTemplate:getColumns:error]", e);
}
@@ -1195,7 +1195,7 @@ async function loadTemplateContext(allowView = false) {
if (dbType === "tdengine") {
try {
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
const tables = await api.listTables(node.connectionId, node.database, querySchema, node.label, 200);
const tables = await api.listTables(node.connectionId, node.database, querySchema, node.label, 200, undefined, undefined, node.catalog);
const matched = tables.find((table) => table.name.toLowerCase() === node.label.toLowerCase());
if (matched?.table_type) tableType = matched.table_type;
} catch (e) {
@@ -1206,8 +1206,8 @@ async function loadTemplateContext(allowView = false) {
return { node, dbType, tableSchema, columns, tableType };
}
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, sql: string, title?: string) {
const tabId = queryStore.createTab(connectionId, database, title, "query", schema);
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, catalog: string | undefined, sql: string, title?: string) {
const tabId = queryStore.createTab(connectionId, database, title, "query", schema, undefined, catalog);
queryStore.updateSql(tabId, sql);
}
@@ -1217,11 +1217,13 @@ async function newSelectTemplate() {
if (!context) return;
const sql = buildTableSelectTemplate({
databaseType: context.dbType,
catalog: context.node.catalog,
database: context.node.database,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
@@ -1233,12 +1235,14 @@ async function newInsertTemplate() {
if (!context) return;
const sql = buildTableInsertTemplate({
databaseType: context.dbType,
catalog: context.node.catalog,
database: context.node.database,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
tableType: context.tableType,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
@@ -1250,11 +1254,13 @@ async function newUpdateTemplate() {
if (!context) return;
const sql = buildTableUpdateTemplate({
databaseType: context.dbType,
catalog: context.node.catalog,
database: context.node.database,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
@@ -1266,11 +1272,13 @@ async function newDeleteTemplate() {
if (!context) return;
const sql = buildTableDeleteTemplate({
databaseType: context.dbType,
catalog: context.node.catalog,
database: context.node.database,
schema: context.tableSchema,
tableName: context.node.label,
columns: context.columns,
});
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, sql);
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
} catch (e: any) {
toast(t("connection.connectFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
}
@@ -1299,7 +1307,7 @@ async function generateDdlTemplate() {
});
}
const formatted = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(currentDatabaseType()), settingsStore.editorSettings.sqlFormatter);
openSqlTemplateTab(node.connectionId, node.database, node.schema, formatted, `DDL - ${node.label}`);
openSqlTemplateTab(node.connectionId, node.database, node.schema, node.catalog, formatted, `DDL - ${node.label}`);
} catch (e: any) {
toast(e?.message || String(e), 5000);
}
@@ -3042,7 +3050,7 @@ function createView() {
const viewName = "new_view";
const effectiveDbType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId));
const viewSqlName = effectiveDbType === "informix" || !node.schema ? viewName : `${node.schema}.${viewName}`;
const tabId = queryStore.createTab(node.connectionId, node.database, t("contextMenu.createView"), "query", node.schema);
const tabId = queryStore.createTab(node.connectionId, node.database, t("contextMenu.createView"), "query", node.schema, undefined, node.catalog);
queryStore.updateSql(tabId, `CREATE VIEW ${viewSqlName} AS\nSELECT\n *\nFROM table_name;\n`);
queryStore.setObjectSource(tabId, {
schema: node.schema,
@@ -3077,7 +3085,7 @@ function createMysqlObjectTemplate() {
const template = mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node);
if (!template) return;
connectionStore.activeConnectionId = node.connectionId;
const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema);
const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema, undefined, node.catalog);
queryStore.updateSql(tabId, template.sql);
}