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
@@ -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]");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user