feat(doris): show external catalogs in sidebar
This commit is contained in:
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]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,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}.`)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user