feat(database): support Doris and StarRocks catalogs
This commit is contained in:
44 files changed
+1330
-242
No files matched your search
@@ -0,0 +1,126 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function starRocksConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "starrocks-1",
|
||||
name: "StarRocks",
|
||||
db_type: "starrocks",
|
||||
host: "127.0.0.1",
|
||||
port: 9030,
|
||||
username: "root",
|
||||
password: "",
|
||||
database: "sales",
|
||||
visible_databases: ["analytics"],
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function catalogNode(connectionId: string, catalog: string, catalogType: string, children: TreeNode[] = []): TreeNode {
|
||||
return {
|
||||
id: `${connectionId}:doris-catalog:${catalog}`,
|
||||
label: catalog,
|
||||
type: "doris-catalog",
|
||||
connectionId,
|
||||
catalog,
|
||||
catalogType,
|
||||
isExpanded: true,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function databaseNode(connectionId: string, catalog: string, database: string): TreeNode {
|
||||
return {
|
||||
id: `${connectionId}:doris-catalog:${catalog}:${database}`,
|
||||
label: database,
|
||||
type: "database",
|
||||
connectionId,
|
||||
catalog,
|
||||
database,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("connectionStore Doris catalog tree", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("applies visible database filtering inside external catalogs", async () => {
|
||||
const listDorisCatalogDatabases = vi.fn().mockResolvedValue([{ name: "app" }, { name: "analytics" }]);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
listDorisCatalogDatabases,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = starRocksConnection();
|
||||
const catalog = catalogNode(connection.id, "hive", "Hive");
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [catalog] }];
|
||||
|
||||
await store.loadTreeNodeChildren(catalog, { force: true });
|
||||
|
||||
expect(catalog.children?.map((node) => [node.catalog, node.database])).toEqual([["hive", "analytics"]]);
|
||||
});
|
||||
|
||||
it("refreshes the selected catalog when database names collide", async () => {
|
||||
const listTables = vi.fn().mockResolvedValue([{ name: "orders", table_type: "BASE TABLE", comment: null }]);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "grouped";
|
||||
const connection = starRocksConnection();
|
||||
const hiveDatabase = databaseNode(connection.id, "hive", "sales");
|
||||
const icebergDatabase = databaseNode(connection.id, "iceberg", "sales");
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
children: [catalogNode(connection.id, "hive", "Hive", [hiveDatabase]), catalogNode(connection.id, "iceberg", "Iceberg", [icebergDatabase])],
|
||||
},
|
||||
];
|
||||
|
||||
await store.refreshObjectListTreeNode(connection.id, "sales", "sales", "iceberg");
|
||||
|
||||
expect(listTables).toHaveBeenCalledTimes(1);
|
||||
expect(listTables.mock.calls[0]?.[7]).toBe("iceberg");
|
||||
expect(hiveDatabase.children).toEqual([]);
|
||||
expect(icebergDatabase.children?.map((node) => [node.catalog, node.database, node.label])).toEqual([["iceberg", "sales", "orders"]]);
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,21 @@ describe("queryStore database open state", () => {
|
||||
expect(store.openTableStructure("doris-1", "sales", undefined, "orders", undefined, undefined, "iceberg_catalog")).toBe(icebergTabId);
|
||||
});
|
||||
|
||||
it("clears the catalog when changing a query tab connection", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.createTab("doris-1", "sales", "Query", "query", undefined, undefined, "hive_catalog");
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
|
||||
store.updateConnection(tabId, "doris-2", "analytics");
|
||||
|
||||
expect(tab.connectionId).toBe("doris-2");
|
||||
expect(tab.catalog).toBeUndefined();
|
||||
expect(tab.database).toBe("analytics");
|
||||
expect(tab.schema).toBeUndefined();
|
||||
});
|
||||
|
||||
it("closes data and structure tabs for a dropped table object", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
@@ -358,6 +358,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const transferSource = ref<{
|
||||
connectionId: string;
|
||||
database: string;
|
||||
catalog?: string;
|
||||
schema?: string;
|
||||
tables?: string[];
|
||||
targetConnectionId?: string;
|
||||
@@ -3849,21 +3850,21 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const config = getConfig(connectionId);
|
||||
const databases = await withMetadataLoadTimeout(connectionId, api.listDorisCatalogDatabases(connectionId, catalog), "databases");
|
||||
const visibleNames = filterDatabaseNamesForConnection(
|
||||
databases.map((database) => database.name),
|
||||
config,
|
||||
);
|
||||
const visibleNameSet = new Set(visibleNames);
|
||||
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
|
||||
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) => {
|
||||
databaseNodes = sortSidebarDatabases(visibleDatabases).flatMap((database) => {
|
||||
const name = database.name.trim();
|
||||
if (!name) return [];
|
||||
return [
|
||||
@@ -5128,24 +5129,31 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await refreshTreeNode(node);
|
||||
}
|
||||
|
||||
async function refreshDatabaseTreeNode(connectionId: string, database: string) {
|
||||
const node = findDatabaseTreeNode(treeNodes.value, connectionId, database);
|
||||
async function refreshDatabaseTreeNode(connectionId: string, database: string, catalog?: string) {
|
||||
const node = findDatabaseTreeNode(treeNodes.value, connectionId, database, catalog);
|
||||
if (node) {
|
||||
await refreshTreeNode(node);
|
||||
return;
|
||||
}
|
||||
if (catalog) {
|
||||
const catalogNode = findNode(treeNodes.value, dorisCatalogId(connectionId, catalog));
|
||||
if (catalogNode) {
|
||||
await refreshTreeNode(catalogNode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await loadDatabases(connectionId, { force: true });
|
||||
}
|
||||
|
||||
async function refreshObjectListTreeNode(connectionId: string, database: string, schema?: string) {
|
||||
async function refreshObjectListTreeNode(connectionId: string, database: string, schema?: string, catalog?: string) {
|
||||
invalidateMetadataCaches({ connectionId, database, schema });
|
||||
const shouldRefreshSchemaNode = !!schema;
|
||||
const shouldRefreshSchemaNode = !!schema && !catalog;
|
||||
const node = shouldRefreshSchemaNode ? findNode(treeNodes.value, `${connectionId}:${database}:${schema}`) : null;
|
||||
if (node) {
|
||||
await refreshTreeNode(node);
|
||||
return;
|
||||
}
|
||||
await refreshDatabaseTreeNode(connectionId, database);
|
||||
await refreshDatabaseTreeNode(connectionId, database, catalog);
|
||||
}
|
||||
|
||||
function isSchemaAwareDatabase(connectionId: string): boolean {
|
||||
|
||||
@@ -2579,6 +2579,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
void closeResultSession(tab);
|
||||
void closeClientConnectionSession(tab);
|
||||
tab.connectionId = connectionId;
|
||||
tab.catalog = undefined;
|
||||
tab.database = database;
|
||||
tab.catalog = undefined;
|
||||
tab.objectBrowser = undefined;
|
||||
|
||||
Reference in new issue
Block a user