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

+12 -4
View File
@@ -1525,9 +1525,14 @@ function changeActiveDatabase(database: string) {
}
}
function changeActiveCatalog(catalog: string | undefined, database: string) {
const tab = activeTab.value;
if (tab) queryStore.updateCatalog(tab.id, catalog, database);
}
async function setActiveDatabaseAsDefault() {
const tab = activeTab.value;
if (!tab || !tab.connectionId || !tab.database) return;
if (!tab || !tab.connectionId || !tab.database || tab.catalog) return;
await connectionStore.setDefaultDatabase(tab.connectionId, tab.database);
}
@@ -1557,9 +1562,11 @@ function ensureQueryTab(): string {
const tab = activeTab.value;
if (tab && tab.mode === "query") return tab.id;
const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id || "";
const db = tab?.connectionId === connId ? tab.database : connectionStore.getConfig(connId)?.database || "";
const schema = tab?.connectionId === connId ? tab.schema : undefined;
return queryStore.createTab(connId, db, undefined, "query", schema);
const sameConnectionTab = tab?.connectionId === connId ? tab : undefined;
const db = sameConnectionTab?.database || connectionStore.getConfig(connId)?.database || "";
const schema = sameConnectionTab?.schema ?? sameConnectionTab?.objectBrowser?.schema ?? sameConnectionTab?.tableMeta?.schema;
const catalog = sameConnectionTab?.catalog ?? sameConnectionTab?.objectBrowser?.catalog ?? sameConnectionTab?.tableMeta?.catalog;
return queryStore.createTab(connId, db, undefined, "query", schema, undefined, catalog);
}
function routeAiRedisCommand(command: string, execute: boolean): boolean {
@@ -2263,6 +2270,7 @@ onUnmounted(() => {
@paste-sql-in-condition="pasteClipboardAsSqlInCondition"
@change-connection="changeActiveConnection"
@change-database="changeActiveDatabase"
@change-catalog="changeActiveCatalog"
@change-schema="changeActiveSchema"
@set-default-database="setActiveDatabaseAsDefault"
@clear-default-database="clearActiveDefaultDatabase"
@@ -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);
}
@@ -1,16 +1,30 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection, fetchSqlFileTargetOptions, namespaceOptionsAreSchemas, useDatabaseOptions } from "@/composables/useDatabaseOptions";
import {
databaseAfterCatalogChange,
databaseOptionsForConnection,
fetchNamespaceOptionsForConnection,
fetchSqlFileTargetOptions,
namespaceOptionsAreSchemas,
normalizedQueryTabCatalog,
queryCatalogSelectorVisible,
selectedQueryCatalogName,
useDatabaseOptions,
} from "@/composables/useDatabaseOptions";
const mocks = vi.hoisted(() => ({
ensureConnected: vi.fn(),
getConfig: vi.fn(),
listDatabases: vi.fn(),
listSchemas: vi.fn(),
listDorisCatalogs: vi.fn(),
listDorisCatalogDatabases: vi.fn(),
}));
vi.mock("@/lib/backend/api", () => ({
listDatabases: mocks.listDatabases,
listSchemas: mocks.listSchemas,
listDorisCatalogs: mocks.listDorisCatalogs,
listDorisCatalogDatabases: mocks.listDorisCatalogDatabases,
}));
vi.mock("@/stores/connectionStore", () => ({
@@ -20,6 +34,47 @@ vi.mock("@/stores/connectionStore", () => ({
}),
}));
const internalCatalog = { name: "default_catalog", catalog_type: "internal", is_current: true };
const paimonCatalog = { name: "paimon_catalog", catalog_type: "paimon", is_current: false };
describe("query catalog options", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("shows the selector only when an external catalog exists", () => {
expect(queryCatalogSelectorVisible([internalCatalog])).toBe(false);
expect(queryCatalogSelectorVisible([internalCatalog, paimonCatalog])).toBe(true);
});
it("maps the internal catalog to an undefined tab catalog", () => {
const catalogs = [internalCatalog, paimonCatalog];
expect(selectedQueryCatalogName(catalogs)).toBe("default_catalog");
expect(normalizedQueryTabCatalog(catalogs, "default_catalog")).toBeUndefined();
expect(normalizedQueryTabCatalog(catalogs, "paimon_catalog")).toBe("paimon_catalog");
});
it("keeps a database only when it exists in the selected catalog", () => {
expect(databaseAfterCatalogChange("bi", ["bi", "default"])).toBe("bi");
expect(databaseAfterCatalogChange("bi", ["analytics"])).toBe("");
});
it("loads and caches catalog-scoped databases", async () => {
mocks.getConfig.mockReturnValue({ db_type: "starrocks" });
mocks.listDorisCatalogs.mockResolvedValue([internalCatalog, paimonCatalog]);
mocks.listDorisCatalogDatabases.mockResolvedValue([{ name: "bi" }, { name: "analytics" }]);
const options = useDatabaseOptions();
await expect(options.loadCatalogOptions("connection-1")).resolves.toEqual([internalCatalog, paimonCatalog]);
await expect(options.loadCatalogDatabaseOptions("connection-1", "paimon_catalog")).resolves.toEqual(["bi", "analytics"]);
await options.loadCatalogDatabaseOptions("connection-1", "paimon_catalog");
expect(options.catalogDatabaseOptions.value["connection-1:paimon_catalog"]).toEqual(["bi", "analytics"]);
expect(mocks.listDorisCatalogDatabases).toHaveBeenCalledTimes(1);
});
});
describe("namespace options", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -2,10 +2,33 @@ import { ref } from "vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { filterDatabaseNamesForConnection, filterSchemaNamesForConnection } from "@/lib/database/visibleDatabases";
import { usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import type { ConnectionConfig } from "@/types/database";
import { isInternalDorisCatalog } from "@/lib/database/databaseFeatureSupport";
import type { CatalogInfo, ConnectionConfig } from "@/types/database";
import * as api from "@/lib/backend/api";
type NamespaceOptionsConnection = Pick<ConnectionConfig, "database" | "db_type" | "driver_profile" | "visible_databases" | "visible_schemas">;
export function catalogDatabaseOptionsKey(connectionId: string, catalog: string): string {
return `${connectionId}:${catalog}`;
}
export function queryCatalogSelectorVisible(catalogs: CatalogInfo[]): boolean {
return catalogs.some((catalog) => !isInternalDorisCatalog(catalog.catalog_type, catalog.name));
}
export function selectedQueryCatalogName(catalogs: CatalogInfo[], tabCatalog?: string): string {
if (tabCatalog) return tabCatalog;
return catalogs.find((catalog) => isInternalDorisCatalog(catalog.catalog_type, catalog.name))?.name ?? "";
}
export function normalizedQueryTabCatalog(catalogs: CatalogInfo[], selectedCatalog: string): string | undefined {
if (!selectedCatalog) return undefined;
const catalog = catalogs.find((candidate) => candidate.name === selectedCatalog);
return catalog && isInternalDorisCatalog(catalog.catalog_type, catalog.name) ? undefined : selectedCatalog;
}
export function databaseAfterCatalogChange(currentDatabase: string, databaseOptions: string[]): string {
return databaseOptions.includes(currentDatabase) ? currentDatabase : "";
}
export function databaseOptionsForConnection(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "visible_databases"> | undefined): string[] {
const names = filterDatabaseNamesForConnection(databaseNames, connection);
@@ -42,6 +65,12 @@ export function useDatabaseOptions() {
const databaseOptions = ref<Record<string, string[]>>({});
const loadingDatabaseOptions = ref<Record<string, boolean>>({});
const catalogOptions = ref<Record<string, CatalogInfo[]>>({});
const loadingCatalogOptions = ref<Record<string, boolean>>({});
const catalogDatabaseOptions = ref<Record<string, string[]>>({});
const loadingCatalogDatabaseOptions = ref<Record<string, boolean>>({});
const catalogRequests = new Map<string, Promise<CatalogInfo[]>>();
const catalogDatabaseRequests = new Map<string, Promise<string[]>>();
async function loadDatabaseOptions(connectionId: string) {
const connection = connectionStore.getConfig(connectionId);
@@ -70,6 +99,56 @@ export function useDatabaseOptions() {
}
}
async function loadCatalogOptions(connectionId: string): Promise<CatalogInfo[]> {
const cached = catalogOptions.value[connectionId];
if (cached) return cached;
const pending = catalogRequests.get(connectionId);
if (pending) return pending;
const request = (async () => {
loadingCatalogOptions.value[connectionId] = true;
try {
await connectionStore.ensureConnected(connectionId);
const catalogs = await api.listDorisCatalogs(connectionId);
catalogOptions.value[connectionId] = catalogs;
return catalogs;
} finally {
loadingCatalogOptions.value[connectionId] = false;
catalogRequests.delete(connectionId);
}
})();
catalogRequests.set(connectionId, request);
return request;
}
async function loadCatalogDatabaseOptions(connectionId: string, catalog: string): Promise<string[]> {
const key = catalogDatabaseOptionsKey(connectionId, catalog);
const cached = catalogDatabaseOptions.value[key];
if (cached) return cached;
const pending = catalogDatabaseRequests.get(key);
if (pending) return pending;
const request = (async () => {
loadingCatalogDatabaseOptions.value[key] = true;
try {
await connectionStore.ensureConnected(connectionId);
const connection = connectionStore.getConfig(connectionId);
const databases = await api.listDorisCatalogDatabases(connectionId, catalog);
const names = databaseOptionsForConnection(
databases.map((database) => database.name),
connection,
);
catalogDatabaseOptions.value[key] = names;
return names;
} finally {
loadingCatalogDatabaseOptions.value[key] = false;
catalogDatabaseRequests.delete(key);
}
})();
catalogDatabaseRequests.set(key, request);
return request;
}
async function getDatabaseOptions(connectionId: string): Promise<string[]> {
if (!databaseOptions.value[connectionId]) {
await loadDatabaseOptions(connectionId);
@@ -77,5 +156,16 @@ export function useDatabaseOptions() {
return databaseOptions.value[connectionId] ?? [];
}
return { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, getDatabaseOptions };
return {
databaseOptions,
loadingDatabaseOptions,
loadDatabaseOptions,
getDatabaseOptions,
catalogOptions,
loadingCatalogOptions,
loadCatalogOptions,
catalogDatabaseOptions,
loadingCatalogDatabaseOptions,
loadCatalogDatabaseOptions,
};
}
+2
View File
@@ -720,6 +720,8 @@ export default {
saveAllChanges: "Save all",
selectConnection: "Select connection",
searchConnection: "Search connections...",
selectCatalog: "Select catalog",
searchCatalog: "Search catalogs...",
selectDatabase: "Select database",
selectDatabaseRequired: "Select a database first",
searchDatabase: "Search databases...",
+2
View File
@@ -700,6 +700,8 @@ export default withEnglishFallback({
saveAllChanges: "Guardar todo",
selectConnection: "Seleccionar conexión",
searchConnection: "Buscar conexiones...",
selectCatalog: "Seleccionar catálogo",
searchCatalog: "Buscar catálogos...",
selectDatabase: "Seleccionar base de datos",
selectDatabaseRequired: "Selecciona una base de datos primero",
searchDatabase: "Buscar bases de datos...",
+2
View File
@@ -698,6 +698,8 @@ export default withEnglishFallback({
saveAllChanges: "Salva tutto",
selectConnection: "Seleziona connessione",
searchConnection: "Cerca connessioni...",
selectCatalog: "Seleziona catalogo",
searchCatalog: "Cerca cataloghi...",
selectDatabase: "Seleziona database",
selectDatabaseRequired: "Seleziona prima un database",
searchDatabase: "Cerca database...",
+2
View File
@@ -698,6 +698,8 @@ export default withEnglishFallback({
saveAllChanges: "すべて保存",
selectConnection: "接続を選択",
searchConnection: "接続を検索...",
selectCatalog: "カタログを選択",
searchCatalog: "カタログを検索...",
selectDatabase: "データベースを選択",
searchDatabase: "データベースを検索...",
selectSchema: "スキーマを選択",
+2
View File
@@ -699,6 +699,8 @@ export default withEnglishFallback({
saveAllChanges: "Salvar tudo",
selectConnection: "Selecionar conexão",
searchConnection: "Pesquisar conexões...",
selectCatalog: "Selecionar catálogo",
searchCatalog: "Pesquisar catálogos...",
selectDatabase: "Selecionar banco de dados",
selectDatabaseRequired: "Selecione um banco de dados primeiro",
searchDatabase: "Pesquisar bancos de dados...",
+2
View File
@@ -721,6 +721,8 @@ export default withEnglishFallback({
saveAllChanges: "全部保存",
selectConnection: "选择连接",
searchConnection: "搜索连接...",
selectCatalog: "选择 Catalog",
searchCatalog: "搜索 Catalog...",
selectDatabase: "选择数据库",
selectDatabaseRequired: "请先选择数据库",
searchDatabase: "搜索数据库...",
+2
View File
@@ -698,6 +698,8 @@ export default withEnglishFallback({
saveAllChanges: "全部儲存",
selectConnection: "選擇連線",
searchConnection: "搜尋連線……",
selectCatalog: "選擇 Catalog",
searchCatalog: "搜尋 Catalog……",
selectDatabase: "選擇資料庫",
selectDatabaseRequired: "請先選擇資料庫",
searchDatabase: "搜尋資料庫……",
+45 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { buildSelectAllSql, isNewQueryPrefillSupported, resolveNewQueryInitialSql, resolveNewQueryTable, type ResolveNewQueryTableInput } from "@/lib/sql/newQueryContext";
import { buildSelectAllSql, isNewQueryPrefillSupported, resolveNewQueryInitialSql, resolveNewQueryTable, resolveNewQueryTarget } from "@/lib/sql/newQueryContext";
import type { ResolveNewQueryTableInput } from "@/lib/sql/newQueryContext";
import type { QueryTab, TreeNode } from "@/types/database";
function dataTab(overrides: Partial<Pick<QueryTab, "mode" | "connectionId" | "database" | "schema" | "tableMeta" | "structureTableName" | "title">> = {}): ResolveNewQueryTableInput["activeTab"] {
@@ -18,6 +19,46 @@ function tableNode(overrides: Partial<Pick<TreeNode, "type" | "connectionId" | "
return { type: "table", connectionId: "conn-1", database: "app_db", schema: "public", tableName: "orders", label: "orders", ...overrides };
}
describe("resolveNewQueryTarget", () => {
it("inherits an external catalog from the active object browser", () => {
expect(
resolveNewQueryTarget({
activeTab: {
connectionId: "conn-1",
database: "bi",
objectBrowser: { catalog: "paimon_catalog" },
},
connections: [{ id: "conn-1", database: "" }],
preferredSource: "tab",
}),
).toEqual({
connectionId: "conn-1",
database: "bi",
schema: undefined,
catalog: "paimon_catalog",
shouldRefreshDefaultDatabase: false,
});
});
it("inherits an external catalog from active table metadata", () => {
expect(
resolveNewQueryTarget({
activeTab: {
connectionId: "conn-1",
database: "bi",
tableMeta: {
catalog: "paimon_catalog",
tableName: "events",
columns: [],
primaryKeys: [],
},
},
connections: [{ id: "conn-1", database: "" }],
})?.catalog,
).toBe("paimon_catalog");
});
});
describe("resolveNewQueryTable", () => {
it("resolves the table from an active data tab", () => {
const table = resolveNewQueryTable({ activeTab: dataTab(), preferredSource: "tab" });
@@ -119,6 +160,9 @@ describe("buildSelectAllSql", () => {
expect(buildSelectAllSql("mysql", { tableName: "a`b" })).toBe("SELECT * FROM `a``b`");
expect(buildSelectAllSql("postgres", { tableName: 'a"b' })).toBe('SELECT * FROM "a""b"');
});
it("qualifies a StarRocks external-catalog table with catalog and database", () => {
expect(buildSelectAllSql("starrocks", { catalog: "paimon_catalog", database: "bi", tableName: "events" })).toBe("SELECT * FROM `paimon_catalog`.`bi`.`events`");
});
});
describe("isNewQueryPrefillSupported", () => {
+10 -4
View File
@@ -813,6 +813,7 @@ export async function executeQuery(
executionId?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -839,6 +840,7 @@ export async function executeMulti(
executionId?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -874,6 +876,7 @@ export async function executeMultiWithProgress(
schema?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -895,20 +898,22 @@ export async function executeMultiWithProgress(
return results;
}
export async function closeQuerySession(connectionId: string, database: string, sessionId: string, clientSessionId?: string): Promise<boolean> {
export async function closeQuerySession(connectionId: string, database: string, sessionId: string, clientSessionId?: string, catalog?: string): Promise<boolean> {
return post("/api/query/close-session", {
connectionId,
database,
sessionId,
clientSessionId,
catalog,
});
}
export async function closeClientConnectionSession(connectionId: string, database: string, clientSessionId: string): Promise<boolean> {
export async function closeClientConnectionSession(connectionId: string, database: string, clientSessionId: string, catalog?: string): Promise<boolean> {
return post("/api/query/close-client-session", {
connectionId,
database,
clientSessionId,
catalog,
});
}
@@ -939,16 +944,17 @@ export async function executeScriptWith2pc(connectionId: string, database: strin
});
}
export async function executeInTransaction(connectionId: string, database: string, statements: string[], schema?: string): Promise<QueryResult> {
export async function executeInTransaction(connectionId: string, database: string, statements: string[], schema?: string, catalog?: string): Promise<QueryResult> {
return post("/api/query/execute-in-transaction", {
connectionId,
database,
statements,
schema,
catalog,
});
}
export async function beginManualTransaction(_connectionId: string, _database: string, _schema?: string): Promise<string> {
export async function beginManualTransaction(_connectionId: string, _database: string, _schema?: string, _catalog?: string): Promise<string> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
+11 -5
View File
@@ -1021,6 +1021,7 @@ export async function executeQuery(
executionId?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -1047,6 +1048,7 @@ export async function executeMulti(
executionId?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -1082,6 +1084,7 @@ export async function executeMultiWithProgress(
schema?: string,
options?: {
maxRows?: number;
catalog?: string;
fetchSize?: number;
pageSize?: number;
resultSessionId?: string;
@@ -1118,20 +1121,22 @@ export async function cancelQuery(executionId: string): Promise<boolean> {
return invoke("cancel_query", { executionId });
}
export async function closeQuerySession(connectionId: string, database: string, sessionId: string, clientSessionId?: string): Promise<boolean> {
export async function closeQuerySession(connectionId: string, database: string, sessionId: string, clientSessionId?: string, catalog?: string): Promise<boolean> {
return invoke("close_query_session", {
connectionId,
database,
sessionId,
clientSessionId,
catalog,
});
}
export async function closeClientConnectionSession(connectionId: string, database: string, clientSessionId: string): Promise<boolean> {
export async function closeClientConnectionSession(connectionId: string, database: string, clientSessionId: string, catalog?: string): Promise<boolean> {
return invoke("close_client_connection_session", {
connectionId,
database,
clientSessionId,
catalog,
});
}
@@ -1158,17 +1163,18 @@ export async function executeScriptWith2pc(connectionId: string, database: strin
});
}
export async function executeInTransaction(connectionId: string, database: string, statements: string[], schema?: string): Promise<QueryResult> {
export async function executeInTransaction(connectionId: string, database: string, statements: string[], schema?: string, catalog?: string): Promise<QueryResult> {
return invoke("execute_in_transaction", {
connectionId,
database,
statements,
schema,
catalog,
});
}
export async function beginManualTransaction(connectionId: string, database: string, schema?: string): Promise<string> {
return invoke("begin_manual_transaction", { connectionId, database, schema });
export async function beginManualTransaction(connectionId: string, database: string, schema?: string, catalog?: string): Promise<string> {
return invoke("begin_manual_transaction", { connectionId, database, schema, catalog });
}
export async function executeInManualTransaction(txnSessionId: string, sql: string, database: string, schema?: string, maxRows?: number): Promise<QueryResult[]> {
+10 -8
View File
@@ -13,7 +13,7 @@ export interface NewQueryTarget {
export type NewQueryContextSource = "tab" | "sidebar";
interface ResolveNewQueryTargetInput {
activeTab?: Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog">;
activeTab?: Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog" | "objectBrowser" | "tableMeta">;
selectedTreeNode?: Pick<TreeNode, "connectionId" | "database" | "schema" | "catalog"> | null;
activeConnectionId?: string | null;
connections: Pick<ConnectionConfig, "id" | "database">[];
@@ -49,16 +49,18 @@ export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQue
: null;
}
function targetFromContext(context: Pick<QueryTab | TreeNode, "connectionId" | "database" | "schema" | "catalog"> | undefined, connections: Pick<ConnectionConfig, "id" | "database">[]): NewQueryTarget | null {
function targetFromContext(context: Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog" | "objectBrowser" | "tableMeta"> | Pick<TreeNode, "connectionId" | "database" | "schema" | "catalog"> | undefined, connections: Pick<ConnectionConfig, "id" | "database">[]): NewQueryTarget | null {
if (!context?.connectionId) return null;
const connection = connections.find((item) => item.id === context.connectionId);
if (!connection) return null;
const database = context.database || resolveDefaultDatabase(connection, []);
const objectBrowser = "objectBrowser" in context ? context.objectBrowser : undefined;
const tableMeta = "tableMeta" in context ? context.tableMeta : undefined;
return {
connectionId: context.connectionId,
database,
schema: "schema" in context ? (context as { schema?: string }).schema : undefined,
catalog: "catalog" in context ? (context as { catalog?: string }).catalog : undefined,
schema: context.schema ?? objectBrowser?.schema ?? tableMeta?.schema,
catalog: context.catalog ?? objectBrowser?.catalog ?? tableMeta?.catalog,
shouldRefreshDefaultDatabase: !context.database,
};
}
@@ -72,7 +74,7 @@ export interface NewQueryTable {
}
export interface ResolveNewQueryTableInput {
activeTab?: Pick<QueryTab, "mode" | "connectionId" | "database" | "schema" | "tableMeta" | "structureTableName" | "title"> | null;
activeTab?: Pick<QueryTab, "mode" | "connectionId" | "database" | "schema" | "catalog" | "tableMeta" | "structureTableName" | "title"> | null;
selectedTreeNode?: Pick<TreeNode, "type" | "connectionId" | "database" | "schema" | "catalog" | "tableName" | "label"> | null;
preferredSource?: NewQueryContextSource;
}
@@ -106,7 +108,7 @@ function tableFromTab(tab: ResolveNewQueryTableInput["activeTab"]): NewQueryTabl
if (tab.mode === "structure") {
const tableName = (tab.structureTableName || "").trim();
if (!tableName) return null;
return { connectionId: tab.connectionId, database: tab.database, schema: tab.schema, tableName };
return { connectionId: tab.connectionId, database: tab.database, schema: tab.schema, catalog: tab.catalog, tableName };
}
return null;
}
@@ -137,8 +139,8 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery
* the same per-dialect identifier quoting and schema/catalog qualification used
* by the table-data view.
*/
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName">): string {
const ref = qualifiedTableName({ databaseType, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>): string {
const ref = qualifiedTableName({ databaseType, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
return `SELECT * FROM ${ref}`;
}
@@ -4,6 +4,8 @@ import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelec
export interface TableSqlTemplateOptions {
databaseType?: DatabaseType;
schema?: string;
catalog?: string;
database?: string;
tableName: string;
columns?: ColumnInfo[];
tableType?: string;
@@ -96,6 +98,8 @@ export function buildTableDeleteTemplate(options: TableSqlTemplateOptions): stri
function templateTableName(options: TableSqlTemplateOptions): string {
return qualifiedTableName({
databaseType: options.databaseType,
catalog: options.catalog,
database: options.database,
schema: options.schema,
tableName: options.tableName,
});
@@ -338,6 +338,16 @@ describe("queryStore multi-statement errors", () => {
expect(mocks.executeMulti).toHaveBeenCalledWith("mysql-1", "app", "SELECT 1", undefined, expect.any(String), expect.objectContaining({ continueOnError: false }));
});
it("passes the selected external catalog to query execution", async () => {
mocks.executeMulti.mockResolvedValue([{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "bi", "Query", "query", undefined, undefined, "paimon_catalog");
await store.executeTabSql(tabId, "SELECT * FROM events");
expect(mocks.executeMulti).toHaveBeenCalledWith("mysql-1", "bi", "SELECT * FROM events", undefined, expect.any(String), expect.objectContaining({ catalog: "paimon_catalog" }));
});
it("keeps old and new executions as result runs, then lets normal execution replace the active run", async () => {
mocks.executeMulti
@@ -1,5 +1,6 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useQueryStore } from "@/stores/queryStore";
describe("queryStore switchTab", () => {
beforeEach(() => {
@@ -74,6 +75,44 @@ describe("queryStore switchTab", () => {
expect(settingsStore.settingsPageActive).toBe(false);
});
it("keeps same-named tabs from different catalogs distinct", async () => {
const queryStore = useQueryStore();
const paimonTabId = queryStore.createTab("sr-1", "bi", "events", "query", undefined, undefined, "paimon_catalog");
const internalTabId = queryStore.createTab("sr-1", "bi", "events", "query", undefined, undefined, "internal");
expect(internalTabId).not.toBe(paimonTabId);
expect(queryStore.tabs).toHaveLength(2);
});
it("preserves catalog context when duplicating a query tab", async () => {
const queryStore = useQueryStore();
const tabId = queryStore.createTab("sr-1", "bi", undefined, "query", undefined, "SELECT 1", "paimon_catalog");
queryStore.duplicateTab(tabId);
expect(queryStore.tabs).toHaveLength(2);
expect(queryStore.tabs[1].catalog).toBe("paimon_catalog");
});
it("switches catalog and database as one query context", () => {
const queryStore = useQueryStore();
const tabId = queryStore.createTab("sr-1", "internal_db", undefined, "query");
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId)!;
tab.result = {
columns: ["id"],
rows: [[1]],
affected_rows: 0,
execution_time_ms: 1,
};
queryStore.updateCatalog(tabId, "paimon_catalog", "bi");
expect(tab.catalog).toBe("paimon_catalog");
expect(tab.database).toBe("bi");
expect(tab.result).toBeUndefined();
});
it("stores local data-grid column filters on the tab result", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
+52 -18
View File
@@ -538,10 +538,11 @@ export const useQueryStore = defineStore("query", () => {
const sessionId = tab?.resultSessionId ?? tab?.result?.session_id;
if (!tab || !sessionId || sessionId === preserveSessionId) return;
try {
const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : undefined;
const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog;
const connection = catalog ? useConnectionStore().getConfig(tab.connectionId) : undefined;
const executionDatabase = dataTabExecutionDatabase(connection, tab.database, catalog);
await api.closeQuerySession(tab.connectionId, executionDatabase, sessionId, tab.id);
if (catalog) await api.closeQuerySession(tab.connectionId, executionDatabase, sessionId, tab.id, catalog);
else await api.closeQuerySession(tab.connectionId, executionDatabase, sessionId, tab.id);
} catch (error) {
console.warn("[DBX][query-session:close:error]", { tabId: tab.id, sessionId, error });
if (throwOnError) throw error;
@@ -555,9 +556,10 @@ export const useQueryStore = defineStore("query", () => {
}
}
async function closeClientSessionId(connectionId: string, database: string, clientSessionId: string, logContext: Record<string, unknown> = {}, throwOnError = false) {
async function closeClientSessionId(connectionId: string, database: string, clientSessionId: string, catalog: string | undefined, logContext: Record<string, unknown> = {}, throwOnError = false) {
try {
await api.closeClientConnectionSession(connectionId, database, clientSessionId);
if (catalog) await api.closeClientConnectionSession(connectionId, database, clientSessionId, catalog);
else await api.closeClientConnectionSession(connectionId, database, clientSessionId);
} catch (error) {
console.warn("[DBX][client-session:close:error]", { ...logContext, clientSessionId, error });
if (throwOnError) throw error;
@@ -566,12 +568,12 @@ export const useQueryStore = defineStore("query", () => {
async function closeClientConnectionSession(tab: QueryTab | undefined, throwOnError = false) {
if (!tab?.connectionId) return;
const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : undefined;
const catalog = tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog;
const connection = catalog ? useConnectionStore().getConfig(tab.connectionId) : undefined;
const executionDatabase = dataTabExecutionDatabase(connection, tab.database, catalog);
const clientSessionIds = [...new Set([tabClientSessionId(tab), ...BACKGROUND_CLIENT_SESSION_SUFFIXES.map((suffix) => tabClientSessionId(tab, suffix)), tab.explainClientSessionId].filter((sessionId): sessionId is string => !!sessionId))];
for (const clientSessionId of clientSessionIds) {
await closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, { tabId: tab.id }, throwOnError);
await closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, catalog, { tabId: tab.id }, throwOnError);
}
}
@@ -1153,13 +1155,13 @@ export const useQueryStore = defineStore("query", () => {
return saveTabs(tabs.value, activeTabId.value);
}
function findTabByIdentity(connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string) {
return tabs.value.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || ""));
function findTabByIdentity(connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, catalog?: string) {
return tabs.value.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
}
function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string, catalog?: string, options: { forceNew?: boolean } = {}) {
if (title && !options.forceNew) {
const existing = findTabByIdentity(connectionId, database, title, mode, schema);
const existing = findTabByIdentity(connectionId, database, title, mode, schema, catalog);
if (existing) {
switchTab(existing.id);
return existing.id;
@@ -1937,6 +1939,7 @@ export const useQueryStore = defineStore("query", () => {
connectionId: original.connectionId,
database: original.database,
schema: original.schema,
catalog: original.catalog,
sql: original.sql,
originalSql: "",
savedSqlId: undefined,
@@ -2409,6 +2412,24 @@ export const useQueryStore = defineStore("query", () => {
tab.tableMeta = undefined;
}
function updateCatalog(id: string, catalog: string | undefined, database: string) {
const tab = tabs.value.find((candidate) => candidate.id === id);
if (!tab || (tab.catalog === catalog && tab.database === database)) return;
rollbackTabTransaction(tab);
void closeResultSession(tab);
void closeClientConnectionSession(tab);
tab.catalog = catalog;
tab.database = database;
tab.schema = undefined;
tab.objectBrowser = undefined;
clearResultPayload(tab);
tab.lastExecutedSql = undefined;
tab.resultBaseSql = undefined;
tab.resultSortedSql = undefined;
clearExplain(tab);
tab.tableMeta = undefined;
}
function updateSchema(id: string, schema: string | undefined) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || tab.schema === schema) return;
@@ -2434,6 +2455,8 @@ export const useQueryStore = defineStore("query", () => {
void closeClientConnectionSession(tab);
tab.connectionId = connectionId;
tab.database = database;
tab.catalog = undefined;
tab.objectBrowser = undefined;
tab.schema = undefined;
clearResultPayload(tab);
tab.lastExecutedSql = undefined;
@@ -2978,6 +3001,7 @@ export const useQueryStore = defineStore("query", () => {
connectionId: string;
database: string;
schema?: string;
catalog?: string;
countSql?: string;
countSqlTarget?: () => Promise<TotalRowCountSqlTarget | undefined>;
result: QueryResult;
@@ -3022,6 +3046,7 @@ export const useQueryStore = defineStore("query", () => {
queryExecutionLog("info", "count:start", { traceId: options.traceId, elapsed: options.elapsed() });
const countResult = await api.executeQuery(options.connectionId, options.database, countTarget.sql, countTarget.schema, countExecutionId, {
clientSessionId,
catalog: options.catalog,
timeoutSecs: options.timeoutSecs,
});
const total = Number(countResult.rows?.[0]?.[0] ?? 0);
@@ -3043,7 +3068,7 @@ export const useQueryStore = defineStore("query", () => {
error,
});
} finally {
void closeClientSessionId(options.connectionId, options.database, clientSessionId, { tabId: options.tabId });
void closeClientSessionId(options.connectionId, options.database, clientSessionId, options.catalog, { tabId: options.tabId });
}
})();
}
@@ -3678,7 +3703,7 @@ export const useQueryStore = defineStore("query", () => {
if (tab.autoCommit === false) {
if (!tab.txnSessionId) {
queryExecutionLog("info", "begin-manual-txn:start", { traceId, elapsed: elapsed() });
tab.txnSessionId = await api.beginManualTransaction(tab.connectionId, executionDatabase, executionSchema);
tab.txnSessionId = await api.beginManualTransaction(tab.connectionId, executionDatabase, executionSchema, tab.catalog);
queryExecutionLog("info", "begin-manual-txn:done", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
}
queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
@@ -3704,6 +3729,7 @@ export const useQueryStore = defineStore("query", () => {
: {}),
...(clientSessionId ? { clientSessionId } : {}),
timeoutSecs: queryTimeoutSecs,
catalog: tab.catalog,
continueOnError: settingsStore.editorSettings.continueOnErrorOnBatch,
};
queryExecutionLog("info", "execute-multi:invoke", {
@@ -3839,6 +3865,7 @@ export const useQueryStore = defineStore("query", () => {
connectionId: current.connectionId,
database: executionDatabase,
schema: current.schema,
catalog: current.catalog,
countSql,
countSqlTarget: dataCountTarget
? async () => ({
@@ -4084,6 +4111,7 @@ export const useQueryStore = defineStore("query", () => {
try {
tableResult = await api.executeQuery(tab.connectionId, tab.database, tableSql, tab.schema, executionId, {
clientSessionId,
catalog: tab.catalog,
timeoutSecs: queryTimeoutSecs,
});
} catch (error: unknown) {
@@ -4095,6 +4123,7 @@ export const useQueryStore = defineStore("query", () => {
try {
tableResult = await api.executeQuery(tab.connectionId, tab.database, tableSql, tab.schema, executionId, {
clientSessionId,
catalog: tab.catalog,
timeoutSecs: queryTimeoutSecs,
});
} catch (fallbackError: unknown) {
@@ -4135,6 +4164,7 @@ export const useQueryStore = defineStore("query", () => {
if (latest?.explainExecutionId === executionId) latest.explainSql = jsonBuilt.sql;
const jsonResult = await api.executeQuery(tab.connectionId, tab.database, jsonBuilt.sql, tab.schema, executionId, {
clientSessionId,
catalog: tab.catalog,
timeoutSecs: queryTimeoutSecs,
});
const current = tabs.value.find((t) => t.id === id);
@@ -4158,7 +4188,7 @@ export const useQueryStore = defineStore("query", () => {
current.explainExecutionId = undefined;
}
if (current?.explainClientSessionId === clientSessionId) current.explainClientSessionId = undefined;
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, { tabId: tab.id, explainExecutionId: executionId });
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id, explainExecutionId: executionId });
}
return { ok: true as const, sql: tab.explainSql ?? tableSql };
}
@@ -4238,7 +4268,7 @@ export const useQueryStore = defineStore("query", () => {
current.explainExecutionId = undefined;
}
if (current?.explainClientSessionId === clientSessionId) current.explainClientSessionId = undefined;
await closeClientSessionId(tab.connectionId, tab.database, clientSessionId, { tabId: tab.id, explainExecutionId: executionId });
await closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id, explainExecutionId: executionId });
}
return { ok: true as const, sql: built.sql };
}
@@ -4257,6 +4287,7 @@ export const useQueryStore = defineStore("query", () => {
try {
const result = await api.executeQuery(tab.connectionId, tab.database, built.sql, tab.schema, executionId, {
clientSessionId,
catalog: tab.catalog,
timeoutSecs: queryTimeoutSecs,
});
const current = tabs.value.find((t) => t.id === id);
@@ -4276,7 +4307,7 @@ export const useQueryStore = defineStore("query", () => {
current.isExplaining = false;
current.explainExecutionId = undefined;
}
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, { tabId: tab.id });
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id });
}
return { ok: true as const, sql: built.sql };
}
@@ -4650,6 +4681,7 @@ export const useQueryStore = defineStore("query", () => {
maxRows: pageLimit,
fetchSize: pageLimit,
clientSessionId,
catalog: tableMeta.catalog,
timeoutSecs: queryTimeoutSecs,
});
const result = results[0];
@@ -4662,7 +4694,7 @@ export const useQueryStore = defineStore("query", () => {
offset += result.rows.length;
}
} finally {
void closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, { tabId: tab.id });
void closeClientSessionId(tab.connectionId, executionDatabase, clientSessionId, tableMeta.catalog, { tabId: tab.id });
}
return {
@@ -4722,9 +4754,10 @@ export const useQueryStore = defineStore("query", () => {
pageSize: plan.pageLimit,
resultSessionId: sessionId,
clientSessionId,
catalog: tab.catalog,
timeoutSecs: queryTimeoutSecs,
}
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, timeoutSecs: queryTimeoutSecs };
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, catalog: tab.catalog, timeoutSecs: queryTimeoutSecs };
const results = await api.executeMulti(tab.connectionId, tab.database, plan.sqlToExecute, tab.schema, exportExecutionId, executionOptions);
const result = results[0];
if (!result) break;
@@ -4738,8 +4771,8 @@ export const useQueryStore = defineStore("query", () => {
offset += result.rows.length;
}
} finally {
if (sessionId) void api.closeQuerySession(tab.connectionId, tab.database, sessionId, clientSessionId);
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, { tabId: tab.id });
if (sessionId) void api.closeQuerySession(tab.connectionId, tab.database, sessionId, clientSessionId, tab.catalog);
void closeClientSessionId(tab.connectionId, tab.database, clientSessionId, tab.catalog, { tabId: tab.id });
}
return {
@@ -4929,6 +4962,7 @@ export const useQueryStore = defineStore("query", () => {
togglePinnedTab,
reorderTab,
updateDatabase,
updateCatalog,
updateSchema,
updateConnection,
setTableMeta,