fix(doris): complete external catalog suggestions

This commit is contained in:
t8y2 committed 2026-07-24 14:11:18 +08:00
1 parent 959943315f
commit 6b98c7ebf1
11 files changed
+254 -81

No files matched your search

@@ -50,4 +50,11 @@ describe("openTabsPersistence originalSql round-trip", () => {
expect(restored.sql).toBe("SELECT 1");
expect(restored.originalSql).toBe("");
});
it("preserves an external Doris catalog across tab restore", () => {
const [restored] = roundTrip([queryTab({ database: "dbx_catalog_completion", catalog: "dbx_mysql_catalog" })]);
expect(restored.database).toBe("dbx_catalog_completion");
expect(restored.catalog).toBe("dbx_mysql_catalog");
});
});
@@ -77,4 +77,27 @@ describe("completionTreeIndex", () => {
expect(completionTablesFromTree(tree, "conn", "hive", "sales_analytics")).toEqual(expected);
expect(completionTablesFromTree(tree, "conn", "hive")).toEqual(expected);
});
it("isolates Doris internal and external catalog tables with the same database name", () => {
const tree: TreeNode[] = [
{
id: "internal-orders",
label: "orders",
type: "table",
connectionId: "doris",
database: "sales",
},
{
id: "hive-orders",
label: "orders",
type: "table",
connectionId: "doris",
catalog: "hive_catalog",
database: "sales",
},
];
expect(completionTablesFromTree(tree, "doris", "sales")).toEqual([{ name: "orders", schema: undefined, type: "table" }]);
expect(completionTablesFromTree(tree, "doris", "sales", undefined, "hive_catalog")).toEqual([{ name: "orders", catalog: "hive_catalog", schema: undefined, type: "table" }]);
});
});
@@ -0,0 +1,25 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const contentAreaSource = readFileSync(new URL("../../../components/layout/ContentArea.vue", import.meta.url), "utf8");
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
const sidebarRuntimeSource = readFileSync(new URL("../../../components/sidebar/SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
const openTabsPersistenceSource = readFileSync(new URL("../../app/openTabsPersistence.ts", import.meta.url), "utf8");
describe("Doris external catalog completion wiring", () => {
it("preserves the selected catalog from the sidebar query action into QueryEditor", () => {
expect(sidebarRuntimeSource).toContain('queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema, undefined, node.catalog)');
expect(contentAreaSource).toContain(':catalog="activeTab.catalog"');
expect(queryEditorSource).toContain("catalog?: string;");
expect(openTabsPersistenceSource).toContain("catalog: tab.catalog");
});
it("includes catalog scope in table and column completion requests", () => {
expect(queryEditorSource).toMatch(/lookupLocalCompletionTables\([\s\S]*?props\.catalog/);
expect(queryEditorSource).toMatch(/listCompletionTables\([\s\S]*?props\.catalog/);
expect(queryEditorSource).toMatch(/listCompletionColumns\([\s\S]*?catalog\)/);
expect(queryEditorSource).toMatch(/watch\(\s*\(\) => props\.catalog,[\s\S]*?refreshCompletionCache\(\)/);
expect(queryEditorSource).toMatch(/function shouldLoadCompletionObjects[\s\S]*?if \(props\.catalog\) return false/);
expect(queryEditorSource).toMatch(/if \(!props\.catalog && props\.databaseType !== "oracle"[\s\S]*?listCompletionObjects/);
});
});
@@ -20,6 +20,7 @@ export interface SavedOpenTab {
customTitle?: boolean;
connectionId: string;
database: string;
catalog?: string;
schema?: string;
sql: string;
originalSql?: string;
@@ -84,6 +85,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
...(tab.customTitle ? { customTitle: true } : {}),
connectionId: tab.connectionId,
database: tab.database,
...(tab.catalog !== undefined ? { catalog: tab.catalog } : {}),
schema: tab.schema,
sql: shouldPersistTabSql(tab) ? tab.sql : "",
// Only round-trip originalSql for plain query tabs (no savedSqlId / externalSqlPath):
@@ -16,14 +16,19 @@ export function completionSchemasFromTree(nodes: readonly TreeNode[], connection
return schemas;
}
export function completionTablesFromTree(nodes: readonly TreeNode[], connectionId: string, database: string, schema?: string): SqlCompletionTable[] {
export function completionTablesFromTree(nodes: readonly TreeNode[], connectionId: string, database: string, schema?: string, catalog?: string): SqlCompletionTable[] {
const preferredSchema = schema?.toLowerCase();
const preferredCatalog = catalog?.toLowerCase();
const tables: SqlCompletionTable[] = [];
visitTree(nodes, (node) => {
if (node.connectionId !== connectionId || node.database !== database || !TABLE_NODE_TYPES.has(node.type)) return;
// External catalogs can contain databases and tables with the same names as
// the internal catalog. Keep the two metadata scopes isolated.
if ((node.catalog?.toLowerCase() ?? undefined) !== preferredCatalog) return;
if (preferredSchema && node.schema?.toLowerCase() !== preferredSchema) return;
tables.push({
name: node.tableName || node.label,
catalog: node.catalog,
schema: node.schema,
type: node.type === "materialized_view" ? "materialized_view" : node.type === "view" ? "view" : "table",
});
@@ -43,7 +48,7 @@ function dedupeCompletionTables(tables: SqlCompletionTable[]): SqlCompletionTabl
const seen = new Set<string>();
const result: SqlCompletionTable[] = [];
for (const table of tables) {
const key = `${table.schema ?? ""}.${table.name}`.toLowerCase();
const key = `${table.catalog ?? ""}.${table.schema ?? ""}.${table.name}`.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
result.push(table);
@@ -1255,6 +1255,7 @@ function sqlAliasKeywordWords(...sources: Array<string | undefined>): string[] {
export interface SqlCompletionTable {
name: string;
catalog?: string;
database?: string;
schema?: string;
type?: SqlObjectNavigationType;