feat(doris): show external catalogs in sidebar

This commit is contained in:
hhzorg authored and GitHub committed 2026-07-09 00:45:29 +08:00
1 parent 47d9257ac2
commit d9bb92d1e9
36 files changed
+1680 -174

No files matched your search

+11 -6
View File
@@ -209,6 +209,7 @@ const props = defineProps<{
sortDirection?: DataGridSortDirection;
sortMode?: DataGridSortMode;
tableMeta?: {
catalog?: string;
schema?: string;
tableName: string;
tableType?: string;
@@ -1323,6 +1324,7 @@ async function loadServerFilterValues(columnIndex: number, searchValue: string)
const columnInfo = tableMeta.columns.find((column) => column.name === columnName);
const sql = await buildDataGridColumnDistinctValuesSql({
databaseType: resolvedDatabaseType.value,
catalog: tableMeta.catalog,
schema: tableMeta.schema,
tableName: tableMeta.tableName,
columnName,
@@ -3683,6 +3685,7 @@ async function buildCurrentCountTarget(): Promise<{ sql: string; schema?: string
if (props.tableMeta) {
const sql = await buildDataGridCountSql({
databaseType: props.databaseType,
catalog: props.tableMeta.catalog,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
whereInput: currentWhereInput(),
@@ -5228,6 +5231,7 @@ async function applyOrderBySearch() {
if (!tableMeta) return;
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
catalog: tableMeta.catalog,
schema: tableMeta.schema,
tableName: tableMeta.tableName,
tableType: tableMeta.tableType,
@@ -5259,6 +5263,7 @@ async function applyWhereFilter() {
if (!tableMeta) return;
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
catalog: tableMeta.catalog,
schema: tableMeta.schema,
tableName: tableMeta.tableName,
tableType: tableMeta.tableType,
@@ -7684,7 +7689,7 @@ async function selectTableInfoTab(tab: TableInfoTab) {
}
watch(
() => [props.tableInfoTab, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName] as const,
() => [props.tableInfoTab, props.connectionId, props.database, props.tableMeta?.catalog, props.tableMeta?.schema, props.tableMeta?.tableName] as const,
([tab]) => {
if (tab) void selectTableInfoTab(tab);
},
@@ -7696,7 +7701,7 @@ async function fetchDdl() {
showTableInfo.value = true;
ddlLoading.value = true;
try {
ddlContent.value = await api.getTableDdl(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName);
ddlContent.value = await api.getTableDdl(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, undefined, props.tableMeta.catalog);
} catch (e: any) {
ddlContent.value = `-- Error: ${e}`;
} finally {
@@ -7709,7 +7714,7 @@ async function fetchIndexes() {
indexesLoading.value = true;
indexesError.value = "";
try {
indexes.value = await api.listIndexes(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName);
indexes.value = await api.listIndexes(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, props.tableMeta.catalog);
indexesLoaded.value = true;
} catch (e: any) {
indexesError.value = String(e?.message || e);
@@ -7728,7 +7733,7 @@ async function fetchForeignKeys() {
foreignKeysLoading.value = true;
foreignKeysError.value = "";
try {
foreignKeys.value = await api.listForeignKeys(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName);
foreignKeys.value = await api.listForeignKeys(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, props.tableMeta.catalog);
foreignKeysLoaded.value = true;
} catch (e: any) {
foreignKeysError.value = String(e?.message || e);
@@ -7742,7 +7747,7 @@ async function fetchTriggers() {
triggersLoading.value = true;
triggersError.value = "";
try {
triggers.value = await api.listTriggers(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName);
triggers.value = await api.listTriggers(props.connectionId, props.database || "", props.tableMeta.schema || props.database || "", props.tableMeta.tableName, props.tableMeta.catalog);
triggersLoaded.value = true;
} catch (e: any) {
triggersError.value = String(e?.message || e);
@@ -7752,7 +7757,7 @@ async function fetchTriggers() {
}
watch(
() => [props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName],
() => [props.connectionId, props.database, props.tableMeta?.catalog, props.tableMeta?.schema, props.tableMeta?.tableName],
() => {
ddlContent.value = "";
indexes.value = [];
@@ -650,15 +650,21 @@ async function toggle() {
})
.catch(() => {});
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = connectionStore.getConfig(node.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await connectionStore.loadSchemas(node.connectionId, node.database);
if (node.catalog && node.catalog !== "internal") {
await connectionStore.loadDorisCatalogTables(node);
} else {
await connectionStore.loadTables(node.connectionId, node.database);
const config = connectionStore.getConfig(node.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await connectionStore.loadSchemas(node.connectionId, node.database);
} else {
await connectionStore.loadTables(node.connectionId, node.database);
}
}
} else if (node.type === "doris-catalog" && node.connectionId) {
await connectionStore.loadDorisCatalogDatabases(node);
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
} else if (node.type === "linked-server-root" && node.connectionId) {
@@ -670,15 +676,15 @@ async function toggle() {
} else if (node.type === "linked-server-schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await connectionStore.loadTables(node.connectionId, node.database, node.schema);
} else if ((node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id, node.catalog);
} else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-indexes" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id);
await connectionStore.loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id);
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (databaseObjectGroup) {
await connectionStore.loadObjectGroupChildren(node);
}
@@ -1176,7 +1182,8 @@ async function openData() {
const querySchema = config ? connectionObjectTreeQuerySchema(config, node.database, tableSchema) : (tableSchema ?? "");
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const metadataDatabaseType = effectiveDbType || config?.db_type || "";
const isSameDataTableTab = (tab: (typeof queryStore.tabs)[number]) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label;
const isSameDataTableTab = (tab: (typeof queryStore.tabs)[number]) =>
tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.tableMeta?.catalog || "") === (node.catalog || "") && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label;
const existingSameTableTab = queryStore.tabs.find(isSameDataTableTab);
const resetReusedDataTabState = (tab: (typeof queryStore.tabs)[number]) => {
tab.title = node.label;
@@ -1250,15 +1257,20 @@ async function openData() {
tableType,
databaseType: metadataDatabaseType,
driverProfile: config.driver_profile || config.db_type,
catalog: node.catalog,
})
: undefined;
const tabCachedTableMeta = existingTableMeta?.tableName === node.label && existingTableMeta.schema === tableSchema && existingTableMeta.tableType === tableType && existingTableMeta.columns.length > 0 && existingTableMetaAgeMs < DATA_TAB_METADATA_TTL_MS ? existingTableMeta : undefined;
const tabCachedTableMeta =
existingTableMeta?.tableName === node.label && (existingTableMeta.catalog || "") === (node.catalog || "") && existingTableMeta.schema === tableSchema && existingTableMeta.tableType === tableType && existingTableMeta.columns.length > 0 && existingTableMetaAgeMs < DATA_TAB_METADATA_TTL_MS
? existingTableMeta
: undefined;
const cachedTableMeta = sharedCachedTableMeta ? tableMetadataToDataTabMeta(sharedCachedTableMeta.metadata, tableSchema) : tabCachedTableMeta;
const cachedTableMetaAgeMs = sharedCachedTableMeta?.ageMs ?? existingTableMetaAgeMs;
const cachedTableMetaSource = sharedCachedTableMeta ? "shared" : tabCachedTableMeta ? "tab" : undefined;
queryStore.setTableMeta(
tabId,
cachedTableMeta ?? {
catalog: node.catalog,
schema: tableSchema,
tableName: node.label,
tableType,
@@ -1306,6 +1318,7 @@ async function openData() {
tableType,
databaseType: metadataDatabaseType,
driverProfile: config.driver_profile || config.db_type,
catalog: node.catalog,
traceLogger: (event) => console.debug("[DBX][openData:metadata:trace]", { sourceTraceId: traceId, ...event }),
});
if (!isCurrentDataTab()) {
@@ -1362,6 +1375,7 @@ async function openData() {
schema: tableSchema,
tableName: node.label,
tableType,
catalog: node.catalog,
columns: columns.map((column) => column.name),
primaryKeys,
limit,
@@ -39,6 +39,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
schema: tableMeta?.schema,
tableName: tableMeta?.tableName ?? "",
tableType: tableMeta?.tableType,
catalog: tableMeta?.catalog,
columns: tableMeta?.columns.map((column) => column.name),
primaryKeys,
includeRowId: useRowId,
@@ -55,6 +56,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
tabId: tab.id,
connectionId: tab.connectionId,
database: tab.database,
catalog: tableMeta.catalog,
schema: tableMeta.schema,
tableName: tableMeta.tableName,
tableType: tableMeta.tableType,
@@ -66,17 +68,18 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const config = connectionStore.getConfig(target.connectionId);
const querySchema = metadataSchemaForConnection(config, target.database, target.schema);
console.info("[DBX][reloadData:metadata:get-columns:start]", { traceId: trace?.traceId, elapsed: trace?.elapsed(), schema: querySchema, table: target.tableName });
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName).catch(() => []);
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName, target.catalog);
const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName, target.catalog).catch(() => []);
console.info("[DBX][reloadData:metadata:get-columns:done]", { traceId: trace?.traceId, elapsed: trace?.elapsed(), columnCount: columns.length });
const current = queryStore.tabs.find((item) => item.id === target.tabId);
const currentMeta = current ? tableMetaForDataTab(current) : undefined;
if (!current || current.mode !== "data" || current.connectionId !== target.connectionId || current.database !== target.database || currentMeta?.tableName !== target.tableName || (currentMeta.schema ?? "") !== (target.schema ?? "")) {
if (!current || current.mode !== "data" || current.connectionId !== target.connectionId || current.database !== target.database || currentMeta?.tableName !== target.tableName || (currentMeta.schema ?? "") !== (target.schema ?? "") || (currentMeta.catalog ?? "") !== (target.catalog ?? "")) {
console.info("[DBX][reloadData:metadata:stale-tab]", { traceId: trace?.traceId, elapsed: trace?.elapsed(), table: target.tableName });
return;
}
const primaryKeys = editableRowIdentifierColumns(effectiveDatabaseTypeForConnection(config), columns, indexes, target.tableType);
queryStore.setTableMeta(target.tabId, {
catalog: target.catalog,
schema: target.schema,
tableName: target.tableName,
tableType: target.tableType,
@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { connectionIsDorisFamilyCatalogCapable, isDorisFamilyCatalogCapable, isInternalDorisCatalog } from "@/lib/database/databaseFeatureSupport";
import type { ConnectionConfig } from "@/types/database";
function conn(db_type: ConnectionConfig["db_type"], driver_profile?: string | null): Pick<ConnectionConfig, "db_type" | "driver_profile"> {
return { db_type, driver_profile: driver_profile ?? null };
}
describe("isDorisFamilyCatalogCapable", () => {
it("matches Doris and StarRocks by db_type", () => {
expect(isDorisFamilyCatalogCapable("doris")).toBe(true);
expect(isDorisFamilyCatalogCapable("starrocks")).toBe(true);
});
it("matches Doris / SelectDB / StarRocks driver profiles", () => {
expect(isDorisFamilyCatalogCapable("mysql", "doris")).toBe(true);
expect(isDorisFamilyCatalogCapable("mysql", "selectdb")).toBe(true);
expect(isDorisFamilyCatalogCapable("mysql", "starrocks")).toBe(true);
});
it("excludes ManticoreSearch (no catalog concept)", () => {
expect(isDorisFamilyCatalogCapable("manticoresearch")).toBe(false);
expect(isDorisFamilyCatalogCapable("mysql", "manticoresearch")).toBe(false);
});
it("excludes plain MySQL / Postgres", () => {
expect(isDorisFamilyCatalogCapable("mysql")).toBe(false);
expect(isDorisFamilyCatalogCapable("postgres")).toBe(false);
});
});
describe("connectionIsDorisFamilyCatalogCapable", () => {
it("returns false for undefined connection", () => {
expect(connectionIsDorisFamilyCatalogCapable(undefined)).toBe(false);
});
it("returns true for a Doris connection", () => {
expect(connectionIsDorisFamilyCatalogCapable(conn("doris"))).toBe(true);
});
it("returns false for a plain MySQL connection", () => {
expect(connectionIsDorisFamilyCatalogCapable(conn("mysql"))).toBe(false);
});
});
describe("isInternalDorisCatalog", () => {
it("detects Doris internal catalog by type", () => {
expect(isInternalDorisCatalog("internal", "internal")).toBe(true);
});
it("detects StarRocks internal catalog by type (case-insensitive)", () => {
expect(isInternalDorisCatalog("Internal", "default_catalog")).toBe(true);
expect(isInternalDorisCatalog("INTERNAL", "default_catalog")).toBe(true);
});
it("treats external catalogs as non-internal", () => {
expect(isInternalDorisCatalog("iceberg", "iceberg_catalog")).toBe(false);
expect(isInternalDorisCatalog("hive", "hive_catalog")).toBe(false);
// A catalog literally named `internal` but with an external type is not built-in.
expect(isInternalDorisCatalog("iceberg", "internal")).toBe(false);
});
it("falls back to the Doris name when the type is absent", () => {
expect(isInternalDorisCatalog("", "internal")).toBe(true);
expect(isInternalDorisCatalog(null, "internal")).toBe(true);
expect(isInternalDorisCatalog("", "default_catalog")).toBe(false);
expect(isInternalDorisCatalog(undefined, undefined)).toBe(false);
});
});
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
describe("qualifiedTableName — Doris/StarRocks multi-catalog", () => {
it("prefixes external catalog for Doris (no schema)", () => {
expect(qualifiedTableName({ databaseType: "doris", catalog: "iceberg_catalog", tableName: "orders" })).toBe("`iceberg_catalog`.`orders`");
});
it("prefixes external catalog for Doris (with schema)", () => {
expect(qualifiedTableName({ databaseType: "doris", catalog: "iceberg_catalog", schema: "sales", tableName: "orders" })).toBe("`iceberg_catalog`.`sales`.`orders`");
});
it("prefixes external catalog for StarRocks", () => {
expect(qualifiedTableName({ databaseType: "starrocks", catalog: "hive_catalog", tableName: "orders" })).toBe("`hive_catalog`.`orders`");
});
it("treats the internal catalog as no catalog", () => {
expect(qualifiedTableName({ databaseType: "doris", catalog: "internal", tableName: "orders" })).toBe("`orders`");
});
it("omits the catalog for non-Doris engines", () => {
// MySQL has no 3-part catalog naming; the catalog must be ignored.
expect(qualifiedTableName({ databaseType: "mysql", catalog: "iceberg_catalog", tableName: "orders" })).toBe("`orders`");
});
it("escapes embedded backticks in catalog and table identifiers", () => {
expect(qualifiedTableName({ databaseType: "doris", catalog: "a`b", schema: "c`d", tableName: "e`f" })).toBe("`a``b`.`c``d`.`e``f`");
});
});
describe("quoteTableIdentifier", () => {
it("backtick-quotes mysql identifiers", () => {
expect(quoteTableIdentifier("mysql", "orders")).toBe("`orders`");
});
it("bracket-quotes sqlserver identifiers", () => {
expect(quoteTableIdentifier("sqlserver", "orders")).toBe("[orders]");
});
});
+2
View File
@@ -107,6 +107,8 @@ export const syncSavedSqlDirectory = forward("syncSavedSqlDirectory");
// Schema
export const listDatabases = forward("listDatabases");
export const listDorisCatalogs = forward("listDorisCatalogs");
export const listDorisCatalogDatabases = forward("listDorisCatalogDatabases");
export const listSqlServerLinkedServers = forward("listSqlServerLinkedServers");
export const listSqlServerLinkedServerCatalogs = forward("listSqlServerLinkedServerCatalogs");
export const listSqlServerLinkedServerSchemas = forward("listSqlServerLinkedServerSchemas");
+24 -14
View File
@@ -3,6 +3,7 @@ import type {
DatabaseInfo,
SchemaInfo,
LinkedServerInfo,
CatalogInfo,
TableInfo,
ObjectInfo,
CompletionAssistantRequest,
@@ -487,6 +488,14 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return get(`/api/schema/databases?${qs({ connection_id: connectionId })}`);
}
export async function listDorisCatalogs(connectionId: string): Promise<CatalogInfo[]> {
return get(`/api/schema/doris/catalogs?${qs({ connection_id: connectionId })}`);
}
export async function listDorisCatalogDatabases(connectionId: string, catalog: string): Promise<DatabaseInfo[]> {
return get(`/api/schema/doris/catalog-databases?${qs({ connection_id: connectionId, catalog })}`);
}
export async function listSqlServerLinkedServers(connectionId: string): Promise<LinkedServerInfo[]> {
return get(`/api/schema/sqlserver/linked-servers?${qs({ connection_id: connectionId })}`);
}
@@ -524,15 +533,15 @@ export async function listSchemaInfos(connectionId: string, database: string): P
return schemas.map((name) => ({ name, comment: null }));
}
export async function listTables(connectionId: string, database: string, schema: string, filter?: string, limit?: number, offset?: number, objectTypes?: SidebarObjectKind[]): Promise<TableInfo[]> {
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema, filter, limit, offset, object_types: objectTypes?.join(",") })}`);
export async function listTables(connectionId: string, database: string, schema: string, filter?: string, limit?: number, offset?: number, objectTypes?: SidebarObjectKind[], catalog?: string): Promise<TableInfo[]> {
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema, filter, limit, offset, object_types: objectTypes?.join(","), catalog })}`);
}
export async function getTableComment(_connectionId: string, _database: string, _schema: string, _table: string): Promise<string | null> {
export async function getTableComment(_connectionId: string, _database: string, _schema: string, _table: string, _catalog?: string): Promise<string | null> {
throw new Error("Table comment lookup is not available in the web backend");
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number): Promise<ObjectInfo[]> {
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
return get(
`/api/schema/objects?${qs({
connection_id: connectionId,
@@ -542,6 +551,7 @@ export async function listObjects(connectionId: string, database: string, schema
filter,
limit,
offset,
catalog,
})}`,
);
}
@@ -562,28 +572,28 @@ export async function getObjectSource(connectionId: string, database: string, sc
return get(`/api/schema/object-source?${qs({ connection_id: connectionId, database, schema, table: name, object_type: objectType })}`);
}
export async function getColumns(connectionId: string, database: string, schema: string, table: string): Promise<ColumnInfo[]> {
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table })}`);
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ColumnInfo[]> {
return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function listDataTypes(connectionId: string, database: string): Promise<string[]> {
return get(`/api/schema/data-types?${qs({ connection_id: connectionId, database })}`);
}
export async function listIndexes(connectionId: string, database: string, schema: string, table: string): Promise<IndexInfo[]> {
return get(`/api/schema/indexes?${qs({ connection_id: connectionId, database, schema, table })}`);
export async function listIndexes(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<IndexInfo[]> {
return get(`/api/schema/indexes?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string): Promise<ForeignKeyInfo[]> {
return get(`/api/schema/foreign-keys?${qs({ connection_id: connectionId, database, schema, table })}`);
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
return get(`/api/schema/foreign-keys?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function listTriggers(connectionId: string, database: string, schema: string, table: string): Promise<TriggerInfo[]> {
return get(`/api/schema/triggers?${qs({ connection_id: connectionId, database, schema, table })}`);
export async function listTriggers(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<TriggerInfo[]> {
return get(`/api/schema/triggers?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
}
export async function getTableDdl(connectionId: string, database: string, schema: string, table: string, objectType?: ObjectSourceKind): Promise<string> {
return get(`/api/schema/ddl?${qs({ connection_id: connectionId, database, schema, table, object_type: objectType })}`);
export async function getTableDdl(connectionId: string, database: string, schema: string, table: string, objectType?: ObjectSourceKind, catalog?: string): Promise<string> {
return get(`/api/schema/ddl?${qs({ connection_id: connectionId, database, schema, table, object_type: objectType, catalog })}`);
}
export async function prepareSchemaDiff(options: SchemaDiffPreparationOptions): Promise<SchemaDiffPreparation> {
+25 -16
View File
@@ -5,6 +5,7 @@ import type {
DatabaseInfo,
SchemaInfo,
LinkedServerInfo,
CatalogInfo,
TableInfo,
ObjectInfo,
CompletionAssistantRequest,
@@ -593,6 +594,14 @@ export async function listDatabases(connectionId: string): Promise<DatabaseInfo[
return invoke("list_databases", { connectionId });
}
export async function listDorisCatalogs(connectionId: string): Promise<CatalogInfo[]> {
return invoke("list_doris_catalogs", { connectionId });
}
export async function listDorisCatalogDatabases(connectionId: string, catalog: string): Promise<DatabaseInfo[]> {
return invoke("list_doris_catalog_databases", { connectionId, catalog });
}
export async function listSqlServerLinkedServers(connectionId: string): Promise<LinkedServerInfo[]> {
return invoke("list_sqlserver_linked_servers", { connectionId });
}
@@ -621,16 +630,16 @@ export async function deleteSchemaCachePrefix(prefix: string): Promise<void> {
return invoke("delete_schema_cache_prefix", { prefix });
}
export async function listTables(connectionId: string, database: string, schema: string, filter?: string, limit?: number, offset?: number, objectTypes?: SidebarObjectKind[]): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema, filter, limit, offset, objectTypes });
export async function listTables(connectionId: string, database: string, schema: string, filter?: string, limit?: number, offset?: number, objectTypes?: SidebarObjectKind[], catalog?: string): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema, filter, limit, offset, objectTypes, catalog });
}
export async function getTableComment(connectionId: string, database: string, schema: string, table: string): Promise<string | null> {
return invoke("get_table_comment", { connectionId, database, schema, table });
export async function getTableComment(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<string | null> {
return invoke("get_table_comment", { connectionId, database, schema, table, catalog });
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number): Promise<ObjectInfo[]> {
return invoke("list_objects", { connectionId, database, schema, objectTypes, filter, limit, offset });
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
return invoke("list_objects", { connectionId, database, schema, objectTypes, filter, limit, offset, catalog });
}
export async function listObjectStatistics(connectionId: string, database: string, schema: string): Promise<ObjectStatistics[]> {
@@ -657,8 +666,8 @@ export async function listSchemaInfos(connectionId: string, database: string): P
return invoke("list_schema_infos", { connectionId, database });
}
export async function getColumns(connectionId: string, database: string, schema: string, table: string): Promise<ColumnInfo[]> {
return invoke("get_columns", { connectionId, database, schema, table });
export async function getColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ColumnInfo[]> {
return invoke("get_columns", { connectionId, database, schema, table, catalog });
}
export async function listDataTypes(connectionId: string, database: string): Promise<string[]> {
@@ -964,20 +973,20 @@ export async function buildDataCompareSyncPlan(options: DataCompareSyncPlanOptio
return invoke("build_data_compare_sync_plan", { options });
}
export async function listIndexes(connectionId: string, database: string, schema: string, table: string): Promise<IndexInfo[]> {
return invoke("list_indexes", { connectionId, database, schema, table });
export async function listIndexes(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<IndexInfo[]> {
return invoke("list_indexes", { connectionId, database, schema, table, catalog });
}
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string): Promise<ForeignKeyInfo[]> {
return invoke("list_foreign_keys", { connectionId, database, schema, table });
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
return invoke("list_foreign_keys", { connectionId, database, schema, table, catalog });
}
export async function listTriggers(connectionId: string, database: string, schema: string, table: string): Promise<TriggerInfo[]> {
return invoke("list_triggers", { connectionId, database, schema, table });
export async function listTriggers(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<TriggerInfo[]> {
return invoke("list_triggers", { connectionId, database, schema, table, catalog });
}
export async function getTableDdl(connectionId: string, database: string, schema: string, table: string, objectType?: ObjectSourceKind): Promise<string> {
return invoke("get_table_ddl", { connectionId, database, schema, table, objectType });
export async function getTableDdl(connectionId: string, database: string, schema: string, table: string, objectType?: ObjectSourceKind, catalog?: string): Promise<string> {
return invoke("get_table_ddl", { connectionId, database, schema, table, objectType, catalog });
}
export async function prepareSchemaDiff(options: SchemaDiffPreparationOptions): Promise<SchemaDiffPreparation> {
@@ -4,6 +4,7 @@ import * as api from "@/lib/backend/api";
export type GridCellValue = string | number | boolean | null | unknown[] | { [key: string]: unknown };
export interface DataGridTableMeta {
catalog?: string;
schema?: string;
tableName: string;
primaryKeys: string[];
@@ -76,6 +77,7 @@ export interface DataGridColumnValuesFilterConditionOptions {
export interface DataGridColumnDistinctValuesSqlOptions {
databaseType?: DatabaseType;
catalog?: string;
schema?: string;
tableName: string;
columnName: string;
@@ -88,6 +90,7 @@ export interface DataGridColumnDistinctValuesSqlOptions {
export interface DataGridCountSqlOptions {
databaseType?: DatabaseType;
catalog?: string;
schema?: string;
tableName: string;
whereInput?: string;
@@ -1,4 +1,4 @@
import type { DatabaseType, TreeNodeType } from "@/types/database";
import type { ConnectionConfig, DatabaseType, TreeNodeType } from "@/types/database";
import { supportsDatabaseFeature } from "@/lib/database/databaseDriverManifest";
import { canEditTableStructure } from "@/lib/table/tableStructureCapabilities";
import { DATABASE_OBJECT_TREE_TYPES, FETCH_FIRST_TYPES, PG_LIKE_STRUCTURE_TYPES, SCHEMA_AWARE_TYPES, SINGLE_DATABASE_TYPES, TREE_SCHEMA_TYPES } from "@/lib/database/databaseCapabilitySets";
@@ -7,6 +7,35 @@ export function isSchemaAware(dbType?: DatabaseType): boolean {
return !!dbType && SCHEMA_AWARE_TYPES.has(dbType);
}
/**
* Doris-family engines that support multi-catalog federation (`SHOW CATALOGS`):
* Doris (incl. SelectDB) and StarRocks. Manticore Search shares the MySQL code
* path but has no catalog concept, so it is excluded.
*/
export function isDorisFamilyCatalogCapable(dbType?: DatabaseType, driverProfile?: string | null): boolean {
if (dbType === "doris" || dbType === "starrocks") return true;
return driverProfile === "doris" || driverProfile === "selectdb" || driverProfile === "starrocks";
}
export function connectionIsDorisFamilyCatalogCapable(connection: Pick<ConnectionConfig, "db_type" | "driver_profile"> | undefined): boolean {
if (!connection) return false;
return isDorisFamilyCatalogCapable(connection.db_type, connection.driver_profile);
}
/**
* Whether a Doris/StarRocks catalog is the engine's built-in (non-federated)
* catalog. Doris names it `internal` (Type=`internal`); StarRocks names it
* `default_catalog` (Type=`Internal`). The `catalogType` column is the
* cross-engine signal, so it is matched case-insensitively, falling back to the
* canonical Doris name `internal` when the type is absent (very old / proxied
* deployments). Mirrors `CatalogInfo::is_internal` on the backend.
*/
export function isInternalDorisCatalog(catalogType?: string | null, catalogName?: string | null): boolean {
const type = (catalogType ?? "").trim().toLowerCase();
if (type) return type === "internal";
return (catalogName ?? "").trim() === "internal";
}
export function usesTreeSchemaMode(dbType?: DatabaseType): boolean {
return !!dbType && TREE_SCHEMA_TYPES.has(dbType);
}
@@ -12,6 +12,7 @@ export interface TableMetadata {
schema?: string;
tableName: string;
tableType?: string;
catalog?: string;
columns: ColumnInfo[];
indexes: IndexInfo[];
primaryKeys: string[];
@@ -26,6 +27,7 @@ export interface TableMetadataRequest {
tableType?: string;
databaseType: DatabaseType | string;
driverProfile?: string;
catalog?: string;
force?: boolean;
traceLogger?: MetadataLoadTraceLogger;
}
@@ -45,7 +47,7 @@ const tableMetadataCoordinator = new MetadataLoadCoordinator((event) => {
console.debug("[DBX][metadata-load:table-coordinator]", event);
});
export function tableMetadataScope(request: Pick<TableMetadataRequest, "connectionId" | "database" | "schema" | "tableName" | "tableType" | "driverProfile" | "databaseType">): MetadataScopeInput {
export function tableMetadataScope(request: Pick<TableMetadataRequest, "connectionId" | "database" | "schema" | "tableName" | "tableType" | "driverProfile" | "databaseType" | "catalog">): MetadataScopeInput {
return {
kind: "table-metadata",
connectionId: request.connectionId,
@@ -54,10 +56,11 @@ export function tableMetadataScope(request: Pick<TableMetadataRequest, "connecti
tableName: request.tableName,
tableType: request.tableType,
driverProfile: request.driverProfile || request.databaseType,
extra: request.catalog ? { catalog: request.catalog } : undefined,
};
}
export function getCachedTableMetadata(request: Pick<TableMetadataRequest, "connectionId" | "database" | "schema" | "tableName" | "tableType" | "driverProfile" | "databaseType">): TableMetadataLoadResult | undefined {
export function getCachedTableMetadata(request: Pick<TableMetadataRequest, "connectionId" | "database" | "schema" | "tableName" | "tableType" | "driverProfile" | "databaseType" | "catalog">): TableMetadataLoadResult | undefined {
const hit = tableMetadataCache.get(tableMetadataScope(request));
if (!hit) return undefined;
return { metadata: hit.value, cacheStatus: hit.stale ? "stale" : "hit", ageMs: hit.ageMs };
@@ -68,6 +71,7 @@ export function tableMetadataToDataTabMeta(metadata: TableMetadata, schema = met
schema,
tableName: metadata.tableName,
tableType: metadata.tableType,
catalog: metadata.catalog,
columns: metadata.columns,
primaryKeys: metadata.primaryKeys,
};
@@ -92,13 +96,14 @@ export async function loadTableMetadata(request: TableMetadataRequest): Promise<
const metadata = await tableMetadataCoordinator.run(
scope,
async () => {
const columns = await api.getColumns(request.connectionId, request.database, request.schema ?? "", request.tableName);
const indexes = await api.listIndexes(request.connectionId, request.database, request.schema ?? "", request.tableName).catch((): IndexInfo[] => []);
const columns = await api.getColumns(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog);
const indexes = await api.listIndexes(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog).catch((): IndexInfo[] => []);
const primaryKeys = editableRowIdentifierColumns(request.databaseType as DatabaseType, columns, indexes, request.tableType);
return {
schema: request.schema || undefined,
tableName: request.tableName,
tableType: request.tableType,
catalog: request.catalog,
columns,
indexes,
primaryKeys,
@@ -33,6 +33,7 @@ const pinnableTypes: Set<TreeNodeType> = new Set([
"linked-server",
"linked-server-catalog",
"linked-server-schema",
"doris-catalog",
"schema",
"table",
"view",
@@ -25,6 +25,8 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
case "linked-server-schema":
case "mq-tenant":
return { icon: FolderOpen, colorClass: "text-sky-400" };
case "doris-catalog":
return { icon: FolderOpen, colorClass: "text-emerald-500" };
case "nacos-namespace":
case "etcd-root":
return { icon: FolderOpen, colorClass: "text-sky-500" };
+16 -3
View File
@@ -16,6 +16,7 @@ export interface BuildTableSelectSqlOptions {
offset?: number;
whereInput?: string;
includeRowId?: boolean;
catalog?: string;
}
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
@@ -23,7 +24,8 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
// JDBC connections use the driver-reported identifier quote string
// (DatabaseMetaData.getIdentifierQuoteString()) — pass through unquoted.
if (databaseType === "jdbc") return name;
if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access") return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access" || databaseType === "doris" || databaseType === "starrocks")
return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
if (databaseType === "neo4j") return quoteCypherIdentifier(name);
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
@@ -34,8 +36,19 @@ function quoteCypherIdentifier(name: string): string {
return `\`${name.replace(/`/g, "``")}\``;
}
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">): string {
const { databaseType, schema, tableName } = options;
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName" | "catalog">): string {
const { databaseType, schema, tableName, catalog } = options;
// Doris / StarRocks multi-catalog: address external-catalog tables with the
// 3-part `catalog.database.table` form, which the engines accept directly.
if (catalog && catalog !== "internal" && (databaseType === "doris" || databaseType === "starrocks")) {
const quotedCatalog = quoteTableIdentifier(databaseType, catalog);
const quotedTable = quoteTableIdentifier(databaseType, tableName);
const trimmedSchema = schema?.trim();
if (trimmedSchema) {
return `${quotedCatalog}.${quoteTableIdentifier(databaseType, trimmedSchema)}.${quotedTable}`;
}
return `${quotedCatalog}.${quotedTable}`;
}
if (databaseType === "iotdb") {
const trimmedSchema = schema?.trim();
if (trimmedSchema && tableName !== trimmedSchema && !tableName.startsWith(`${trimmedSchema}.`)) {
+6 -1
View File
@@ -7,7 +7,7 @@ export function normalizeDatabaseObjectName(name: string): string {
return name.trim();
}
export function buildTableTreeNodes({ nodeId, connectionId, database, schema, tables }: { nodeId: string; connectionId: string; database: string; schema?: string; tables: TableInfo[] }): TreeNode[] {
export function buildTableTreeNodes({ nodeId, connectionId, database, schema, tables, catalog }: { nodeId: string; connectionId: string; database: string; schema?: string; tables: TableInfo[]; catalog?: string }): TreeNode[] {
const entries = tables.flatMap((table) => {
const name = normalizeDatabaseObjectName(table.name);
if (!name) return [];
@@ -26,6 +26,7 @@ export function buildTableTreeNodes({ nodeId, connectionId, database, schema, ta
comment: table.comment,
parentSchema: table.parent_schema,
parentName: table.parent_name,
catalog,
}),
];
});
@@ -54,6 +55,7 @@ function makeTableTreeEntry({
comment,
parentSchema,
parentName,
catalog,
}: {
nodeId: string;
connectionId: string;
@@ -66,6 +68,7 @@ function makeTableTreeEntry({
comment?: string | null;
parentSchema?: string | null;
parentName?: string | null;
catalog?: string;
}): TableTreeEntry {
const normalizedParentSchema = parentSchema ? normalizeDatabaseObjectName(parentSchema) : undefined;
const normalizedParentName = parentName ? normalizeDatabaseObjectName(parentName) : undefined;
@@ -79,6 +82,7 @@ function makeTableTreeEntry({
connectionId,
database,
schema,
catalog,
isExpanded: false,
children: [],
};
@@ -293,6 +297,7 @@ function buildPartitionTree(entries: TableTreeEntry[], connectionId: string, dat
connectionId,
database,
schema: entry.schema,
catalog: entry.node.catalog,
tableName: entry.node.label,
objectCount: partitionChildren.length,
isExpanded: false,
+253 -65
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { uuid } from "@/lib/common/utils";
import { ref, computed, watch } from "vue";
import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SchemaInfo, SidebarLayout, TableInfo, TreeNode, VectorCollectionMeta } from "@/types/database";
import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, CatalogInfo, ForeignKeyInfo, ObjectInfo, SchemaInfo, SidebarLayout, TableInfo, TreeNode, VectorCollectionMeta } from "@/types/database";
import { applyPinnedTreeNodeState, updatePinnedTreeNodeInPlace } from "@/lib/app/pinnedItems";
import {
reconcileLayout,
@@ -23,9 +23,9 @@ import {
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
import { collapseExpandedTreeNodes } from "@/lib/sidebar/sidebarTreeCollapse";
import { findDatabaseTreeNode } from "@/lib/sidebar/treeRefreshTarget";
@@ -121,6 +121,14 @@ function sqlServerLinkedCatalogId(connectionId: string, server: string, catalog:
return `${sqlServerLinkedServerId(connectionId, server)}:${nodeIdPart(catalog)}`;
}
function dorisCatalogId(connectionId: string, catalog: string): string {
return `${connectionId}:doris-catalog:${nodeIdPart(catalog)}`;
}
function dorisCatalogDatabaseId(connectionId: string, catalog: string, database: string): string {
return `${dorisCatalogId(connectionId, catalog)}:${nodeIdPart(database)}`;
}
function sqlServerLinkedRuntimeDatabase(config?: ConnectionConfig): string {
return config?.database?.trim() || "master";
}
@@ -2027,49 +2035,87 @@ export const useConnectionStore = defineStore("connection", () => {
setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node));
await savePersistedConnectionTreeChildren(cacheKey, node.children || schemaNodes);
} else {
const cacheKey = schemaCacheKey(connectionId, "databases");
if (!options?.force) {
const cached = await loadPersistedTreeChildren(node, cacheKey);
if (cached.hit) {
if (cached.isStale) refreshStaleTreeNode(node);
return;
}
}
const databases = await withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases");
const visibleNames = filterDatabaseNamesForConnection(
databases.map((database) => database.name),
config,
);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const databaseNodes = buildDatabaseTreeNodes(connectionId, visibleDatabases, {
includeDefaultWhenEmpty: usesTreeSchemaMode(effectiveDbType) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
});
if (config?.db_type === "sqlserver") {
const linkedServers = await withMetadataLoadTimeout(connectionId, api.listSqlServerLinkedServers(connectionId), "linked servers").catch(() => []);
const linkedDatabase = sqlServerLinkedRuntimeDatabase(config);
databaseNodes.push({
...sqlServerLinkedRootNode(connectionId, linkedDatabase),
children: linkedServers.map((server) => ({
id: sqlServerLinkedServerId(connectionId, server.name),
label: server.name,
type: "linked-server",
connectionId,
database: linkedDatabase,
linkedServer: server.name,
comment: [server.product, server.provider, server.data_source].filter(Boolean).join(" / ") || null,
isExpanded: false,
children: [],
})),
// Doris / StarRocks multi-catalog: when external catalogs exist,
// render a catalog grouping layer (catalog → database → tables).
// When only `internal` is present, fall through to the flat database
// list (no regression for single-catalog deployments).
let dorisCatalogs: CatalogInfo[] | null = null;
if (connectionIsDorisFamilyCatalogCapable(config)) {
dorisCatalogs = await withMetadataLoadTimeout(connectionId, api.listDorisCatalogs(connectionId), "catalogs").catch((error: unknown) => {
recordMetadataLoadError(connectionId, error);
return null;
});
if (linkedServers.length > 0) loadedTreeNodeChildrenIds.value.add(sqlServerLinkedRootId(connectionId));
}
const children = withSavedSqlRoot(connectionId, databaseNodes, node);
if (isSidebarSearchQueryChanged(options)) return;
if (!canApplyTreeMetadataResult(node)) return;
setChildren(node, children);
await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
if (dorisCatalogs && dorisCatalogs.length > 1) {
const cacheKey = schemaCacheKey(connectionId, "doris-catalogs");
if (!options?.force) {
const cached = await loadPersistedTreeChildren(node, cacheKey);
if (cached.hit) {
if (cached.isStale) refreshStaleTreeNode(node);
return;
}
}
const catalogNodes: TreeNode[] = dorisCatalogs.map((catalog) => ({
id: dorisCatalogId(connectionId, catalog.name),
label: catalog.name,
type: "doris-catalog" as const,
connectionId,
catalog: catalog.name,
catalogType: catalog.catalog_type,
comment: catalog.comment ?? null,
isExpanded: false,
children: [],
}));
const children = withSavedSqlRoot(connectionId, catalogNodes, node);
if (isSidebarSearchQueryChanged(options)) return;
if (!canApplyTreeMetadataResult(node)) return;
setChildren(node, children);
await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
} else {
const cacheKey = schemaCacheKey(connectionId, "databases");
if (!options?.force) {
const cached = await loadPersistedTreeChildren(node, cacheKey);
if (cached.hit) {
if (cached.isStale) refreshStaleTreeNode(node);
return;
}
}
const databases = await withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases");
const visibleNames = filterDatabaseNamesForConnection(
databases.map((database) => database.name),
config,
);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const databaseNodes = buildDatabaseTreeNodes(connectionId, visibleDatabases, {
includeDefaultWhenEmpty: usesTreeSchemaMode(effectiveDbType) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
});
if (config?.db_type === "sqlserver") {
const linkedServers = await withMetadataLoadTimeout(connectionId, api.listSqlServerLinkedServers(connectionId), "linked servers").catch(() => []);
const linkedDatabase = sqlServerLinkedRuntimeDatabase(config);
databaseNodes.push({
...sqlServerLinkedRootNode(connectionId, linkedDatabase),
children: linkedServers.map((server) => ({
id: sqlServerLinkedServerId(connectionId, server.name),
label: server.name,
type: "linked-server",
connectionId,
database: linkedDatabase,
linkedServer: server.name,
comment: [server.product, server.provider, server.data_source].filter(Boolean).join(" / ") || null,
isExpanded: false,
children: [],
})),
});
if (linkedServers.length > 0) loadedTreeNodeChildrenIds.value.add(sqlServerLinkedRootId(connectionId));
}
const children = withSavedSqlRoot(connectionId, databaseNodes, node);
if (isSidebarSearchQueryChanged(options)) return;
if (!canApplyTreeMetadataResult(node)) return;
setChildren(node, children);
await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
}
}
node.isExpanded = true;
} catch (e) {
@@ -2695,6 +2741,136 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
// Doris / StarRocks multi-catalog: load the databases under a catalog node.
async function loadDorisCatalogDatabases(node: TreeNode, options?: LoadTreeOptions) {
if (!node.connectionId || !node.catalog) return;
const connectionId = node.connectionId;
const catalog = node.catalog;
node.isLoading = true;
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const config = getConfig(connectionId);
const databases = await withMetadataLoadTimeout(connectionId, api.listDorisCatalogDatabases(connectionId, catalog), "databases");
let databaseNodes: TreeNode[];
if (isInternalDorisCatalog(node.catalogType, catalog)) {
// The internal catalog's databases are rendered as standard database
// nodes so they reuse the existing table-loading / table-open paths.
// Detection is type-based (catalogType=`internal`), so StarRocks
// `default_catalog` routes here too — its tables carry no catalog.
const visibleNames = filterDatabaseNamesForConnection(
databases.map((database) => database.name),
config,
);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
databaseNodes = buildDatabaseTreeNodes(connectionId, visibleDatabases, { includeDefaultWhenEmpty: false });
} else {
databaseNodes = sortSidebarDatabases(databases).flatMap((database) => {
const name = database.name.trim();
if (!name) return [];
return [
{
id: dorisCatalogDatabaseId(connectionId, catalog, name),
label: name,
type: "database" as const,
connectionId,
database: name,
catalog,
isExpanded: false,
children: [],
},
];
});
}
if (isSidebarSearchQueryChanged(options)) return;
if (!canApplyTreeMetadataResult(node)) return;
setChildren(node, databaseNodes);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
// Doris / StarRocks multi-catalog: load tables under an external-catalog
// database node. External catalogs only expose tables/views, so a flat list
// (no routines/sequences/triggers) is rendered.
async function loadDorisCatalogTables(node: TreeNode, options?: LoadTreeOptions) {
if (!node.connectionId || !node.database || !node.catalog) return;
const connectionId = node.connectionId;
const { database, catalog } = node;
const configForScope = getConfig(connectionId);
const simpleObjectDisplayForScope = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
const objectTypesForScope = simpleObjectDisplayForScope ? supportedSidebarObjectTypes(configForScope) : undefined;
return runTreeMetadataLoad(
{
kind: "schema-tables",
connectionId,
database,
schema: undefined,
nodeKind: "database",
objectTypes: objectTypesForScope,
searchFilter: activeTreeLoadSearchFilter(options),
limit: simpleObjectDisplayForScope ? sidebarObjectGroupPageSize() + 1 : undefined,
offset: 0,
sidebarDisplayMode: simpleObjectDisplayForScope ? "simple" : "grouped",
driverProfile: metadataDriverProfile(configForScope),
extra: options?.sidebarTableSearchParentId ? { sidebarTableSearchParentId: options.sidebarTableSearchParentId } : undefined,
},
async () => {
node.isLoading = true;
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
const searchFilter = activeTreeLoadSearchFilter(options);
const cacheKey = schemaCacheKey(connectionId, `doris-catalog:${catalog}`, database, "objects-simple-v3");
if (!options?.force && !searchFilter) {
const cached = await loadPersistedTreeChildren(node, cacheKey);
if (cached.hit) {
if (cached.isStale) refreshStaleTreeNode(node);
return;
}
}
const pageSize = sidebarObjectGroupPageSize();
const fetchLimit = searchFilter ? pageSize : pageSize + 1;
const fetchOffset = searchFilter ? undefined : 0;
const tables = await withMetadataLoadTimeout(connectionId, api.listTables(connectionId, database, "", searchFilter, fetchLimit, fetchOffset, objectTypesForScope, catalog), "tables");
const hasMore = searchFilter ? false : tables.length > pageSize;
const pageTables = hasMore ? tables.slice(0, pageSize) : tables;
indexCompletionTables(connectionId, database, undefined, tableInfosToCompletionTables(pageTables, undefined));
let children = buildTableTreeNodes({
nodeId: node.id,
connectionId,
database,
schema: undefined,
tables: pageTables,
catalog,
});
if (hasMore && !searchFilter) {
children = [...children, buildLoadMoreNode(node, pageSize, pageSize)];
}
if (isTreeLoadSearchChanged(searchFilter, options)) return;
if (!canApplyTreeMetadataResult(node)) return;
node.objectCount = children.filter((child) => child.type !== "load-more").length;
setChildren(node, children);
if (!searchFilter && !options?.sidebarTableSearchParentId) {
await savePersistedTreeChildren(cacheKey, children);
}
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
},
options,
);
}
async function loadTables(connectionId: string, database: string, schema?: string, options?: LoadTreeOptions) {
const configForScope = getConfig(connectionId);
const simpleObjectDisplayForScope = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
@@ -3181,7 +3357,7 @@ export const useConnectionStore = defineStore("connection", () => {
return normalizeSidebarObjectKind(type);
}
async function loadTableGroups(connectionId: string, database: string, table: string, schema?: string, nodeId?: string) {
async function loadTableGroups(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string) {
const parentId = nodeId ?? (schema ? `${connectionId}:${database}:${schema}:${table}` : `${connectionId}:${database}:${table}`);
const node = findNode(treeNodes.value, parentId);
if (!node) return;
@@ -3195,6 +3371,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
catalog,
tableName: table,
isExpanded: false,
children: [],
@@ -3212,6 +3389,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
catalog,
tableName: table,
isExpanded: false,
children: [],
@@ -3227,6 +3405,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
catalog,
tableName: table,
isExpanded: false,
children: [],
@@ -3240,6 +3419,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId,
database,
schema,
catalog,
tableName: table,
isExpanded: false,
children: [],
@@ -3251,7 +3431,7 @@ export const useConnectionStore = defineStore("connection", () => {
node.isExpanded = true;
}
async function loadColumns(connectionId: string, database: string, table: string, schema?: string, nodeId?: string) {
async function loadColumns(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string) {
const parentId = nodeId ?? (schema ? `${connectionId}:${database}:${schema}:${table}:__columns` : `${connectionId}:${database}:${table}:__columns`);
const node = findNode(treeNodes.value, parentId);
if (!node) return;
@@ -3286,7 +3466,7 @@ export const useConnectionStore = defineStore("connection", () => {
return;
}
const querySchema = metadataQuerySchema(connectionId, database, schema);
const columns = await api.getColumns(connectionId, database, querySchema, table);
const columns = await api.getColumns(connectionId, database, querySchema, table, catalog);
setChildren(
node,
columns.map((col) => ({
@@ -3309,7 +3489,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadIndexes(connectionId: string, database: string, table: string, schema?: string, nodeId?: string) {
async function loadIndexes(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string) {
const parentId = nodeId ?? (schema ? `${connectionId}:${database}:${schema}:${table}:__indexes` : `${connectionId}:${database}:${table}:__indexes`);
const node = findNode(treeNodes.value, parentId);
if (!node) return;
@@ -3323,7 +3503,7 @@ export const useConnectionStore = defineStore("connection", () => {
return;
}
const querySchema = metadataQuerySchema(connectionId, database, schema);
const indexes = await api.listIndexes(connectionId, database, querySchema, table);
const indexes = await api.listIndexes(connectionId, database, querySchema, table, catalog);
setChildren(
node,
indexes.map((idx) => ({
@@ -3346,7 +3526,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadForeignKeys(connectionId: string, database: string, table: string, schema?: string, nodeId?: string) {
async function loadForeignKeys(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string) {
const parentId = nodeId ?? (schema ? `${connectionId}:${database}:${schema}:${table}:__fkeys` : `${connectionId}:${database}:${table}:__fkeys`);
const node = findNode(treeNodes.value, parentId);
if (!node) return;
@@ -3360,7 +3540,7 @@ export const useConnectionStore = defineStore("connection", () => {
return;
}
const querySchema = metadataQuerySchema(connectionId, database, schema);
const fkeys = await api.listForeignKeys(connectionId, database, querySchema, table);
const fkeys = await api.listForeignKeys(connectionId, database, querySchema, table, catalog);
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`;
completionForeignKeysCache.value[cacheKey] = fkeys;
evictOldestCacheEntries(completionForeignKeysCache.value, COMPLETION_CACHE_MAX);
@@ -3387,7 +3567,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadTriggers(connectionId: string, database: string, table: string, schema?: string, nodeId?: string) {
async function loadTriggers(connectionId: string, database: string, table: string, schema?: string, nodeId?: string, catalog?: string) {
const parentId = nodeId ?? (schema ? `${connectionId}:${database}:${schema}:${table}:__triggers` : `${connectionId}:${database}:${table}:__triggers`);
const node = findNode(treeNodes.value, parentId);
if (!node) return;
@@ -3401,7 +3581,7 @@ export const useConnectionStore = defineStore("connection", () => {
return;
}
const querySchema = metadataQuerySchema(connectionId, database, schema);
const triggers = await api.listTriggers(connectionId, database, querySchema, table);
const triggers = await api.listTriggers(connectionId, database, querySchema, table, catalog);
setChildren(
node,
triggers.map((tr) => ({
@@ -3464,15 +3644,21 @@ export const useConnectionStore = defineStore("connection", () => {
await loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
} else if (node.type === "mongo-gridfs") {
node.isExpanded = true;
} else if (node.type === "doris-catalog" && node.connectionId) {
await loadDorisCatalogDatabases(node, options);
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = getConfig(node.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await loadSchemas(node.connectionId, node.database, options);
if (node.catalog && node.catalog !== "internal") {
await loadDorisCatalogTables(node, options);
} else {
await loadTables(node.connectionId, node.database, undefined, options);
const config = getConfig(node.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await loadSchemas(node.connectionId, node.database, options);
} else {
await loadTables(node.connectionId, node.database, undefined, options);
}
}
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await loadTables(node.connectionId, node.database, node.schema, options);
@@ -3485,15 +3671,15 @@ export const useConnectionStore = defineStore("connection", () => {
} else if (node.type === "linked-server-schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
await loadTables(node.connectionId, node.database, node.schema, options);
} else if ((node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
await loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
await loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id, node.catalog);
} else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id);
await loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-indexes" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id);
await loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id);
await loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
await loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id);
await loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
} else if (node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views" || node.type === "group-procedures" || node.type === "group-functions" || node.type === "group-sequences" || node.type === "group-packages") {
await loadObjectGroupChildren(node, options);
} else if (node.type === "group-partitions") {
@@ -4837,6 +5023,8 @@ export const useConnectionStore = defineStore("connection", () => {
loadSqlServerLinkedServers,
loadSqlServerLinkedServerCatalogs,
loadSqlServerLinkedServerSchemas,
loadDorisCatalogDatabases,
loadDorisCatalogTables,
loadTables,
loadTableForLocate,
loadObjectGroupChildren,
+1
View File
@@ -2880,6 +2880,7 @@ export const useQueryStore = defineStore("query", () => {
schema: tableMeta.schema,
tableName: tableMeta.tableName,
tableType: tableMeta.tableType,
catalog: tableMeta.catalog,
columns: tableMeta.columns.map((column) => column.name),
primaryKeys,
whereInput: tab.whereInput,
+12
View File
@@ -299,6 +299,14 @@ export interface LinkedServerInfo {
data_source?: string | null;
}
/** A catalog exposed by a multi-catalog engine (Doris / StarRocks). */
export interface CatalogInfo {
name: string;
catalog_type: string;
is_current: boolean;
comment?: string | null;
}
export interface TableInfo {
name: string;
table_type: string;
@@ -513,6 +521,7 @@ export type TreeNodeType =
| "connection"
| "connection-group"
| "database"
| "doris-catalog"
| "linked-server-root"
| "linked-server"
| "linked-server-catalog"
@@ -589,6 +598,8 @@ export interface TreeNode {
pinned?: boolean;
connectionId?: string;
database?: string;
catalog?: string;
catalogType?: string;
linkedServer?: string;
linkedCatalog?: string;
linkedSchema?: string;
@@ -714,6 +725,7 @@ export interface QueryTab {
schema?: string;
tableName: string;
tableType?: string;
catalog?: string;
columns: ColumnInfo[];
primaryKeys: string[];
};