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

@@ -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,