feat(completion): show table metadata comments in SQL completions

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-24 20:36:58 +08:00
1 parent f45fcdb69d
commit 8d23216d60
7 files changed
+104 -49

No files matched your search

@@ -41,6 +41,7 @@ describe("completionTreeIndex", () => {
connectionId: "conn",
database: "hive",
schema: "sales_analytics",
comment: "Daily revenue summary",
},
{
id: "conn:hive:sales_analytics:__tables:daily_revenue_view",
@@ -70,7 +71,7 @@ describe("completionTreeIndex", () => {
expect(completionSchemasFromTree(tree, "conn", "hive")).toEqual(["sales_analytics"]);
const expected = [
{ name: "daily_revenue", schema: "sales_analytics", type: "table" },
{ name: "daily_revenue", schema: "sales_analytics", type: "table", detail: "→ Daily revenue summary" },
{ name: "daily_revenue_view", schema: "sales_analytics", type: "view" },
{ name: "daily_revenue_mv", schema: "sales_analytics", type: "materialized_view" },
];
@@ -801,6 +801,52 @@ describe("sqlCompletion scoped context classification", () => {
});
describe("sqlCompletion scoped metadata ranking", () => {
it("shows a table metadata detail in the completion item", () => {
const sql = "SELECT * FROM orders";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [{ name: "orders", type: "table", detail: "→ Customer orders" }],
columnsByTable: new Map(),
}).filter((item) => item.type === "table");
expect(items).toEqual([expect.objectContaining({ label: "orders", detail: "→ Customer orders" })]);
});
it("hides the redundant table type while preserving view and schema details", () => {
const simpleItems = buildSqlCompletionItems("SELECT * FROM orders", "SELECT * FROM orders".length, {
tables: [{ name: "orders", schema: "public", type: "table" }],
columnsByTable: new Map(),
}).filter((item) => item.type === "table");
expect(simpleItems.find((item) => item.label === "orders")?.detail).toBeUndefined();
const viewItems = buildSqlCompletionItems("SELECT * FROM order_view", "SELECT * FROM order_view".length, {
tables: [{ name: "order_view", schema: "public", type: "view" }],
columnsByTable: new Map(),
}).filter((item) => item.type === "table");
expect(viewItems.find((item) => item.label === "order_view")?.detail).toBe("view");
const duplicateItems = buildSqlCompletionItems("SELECT * FROM orders", "SELECT * FROM orders".length, {
tables: [
{ name: "orders", schema: "archive", type: "table" },
{ name: "orders", schema: "sales", type: "table" },
],
columnsByTable: new Map(),
}).filter((item) => item.type === "table");
expect(duplicateItems.map((item) => item.detail).sort()).toEqual(["archive.orders", "sales.orders"]);
const annotatedDuplicateItems = buildSqlCompletionItems("SELECT * FROM orders", "SELECT * FROM orders".length, {
tables: [
{ name: "orders", schema: "archive", type: "table", detail: "→ Archived orders" },
{ name: "orders", schema: "sales", type: "table", detail: "→ Current orders" },
],
columnsByTable: new Map(),
}).filter((item) => item.type === "table");
expect(annotatedDuplicateItems.map((item) => item.detail).sort()).toEqual(["archive.orders → Archived orders", "sales.orders → Current orders"]);
});
it("ranks exact and prefix table matches ahead of contains/fuzzy matches", () => {
const sql = "SELECT * FROM Temp";
const items = buildSqlCompletionItems(sql, sql.length, {
@@ -4,6 +4,11 @@ import { isTdengineStableTableType } from "@/lib/table/tableEditing";
const TABLE_NODE_TYPES = new Set(["table", "view", "materialized_view"]);
function completionTableDetail(comment: string | null | undefined): string | undefined {
const normalized = comment?.trim();
return normalized ? `→ ${normalized}` : undefined;
}
export function completionSchemasFromTree(nodes: readonly TreeNode[], connectionId: string, database: string): string[] {
const seen = new Set<string>();
const schemas: string[] = [];
@@ -27,11 +32,13 @@ export function completionTablesFromTree(nodes: readonly TreeNode[], connectionI
// the internal catalog. Keep the two metadata scopes isolated.
if ((node.catalog?.toLowerCase() ?? undefined) !== preferredCatalog) return;
if (preferredSchema && node.schema?.toLowerCase() !== preferredSchema) return;
const detail = completionTableDetail(node.comment);
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",
...(detail ? { detail } : {}),
...(isTdengineStableTableType(node.tableType) ? { tableType: node.tableType } : {}),
});
});
+3 -1
View File
@@ -3372,10 +3372,12 @@ function buildTableItems(
const suppliedApplyNameIsQualified = suppliedApplyName?.includes(".") === true;
const applyName = qualifiedByContext ? quoteCompletionApplyIdentifier(table.name, dialect) : ambiguousTableName && !!table.schema && (!suppliedApplyName || !suppliedApplyNameIsQualified) ? defaultApplyName : (suppliedApplyName ?? defaultApplyName);
const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : "";
const schemaDetail = ambiguousTableName && table.schema ? `${table.schema}.${table.name}` : undefined;
const detail = table.detail && schemaDetail ? `${schemaDetail} ${table.detail}` : (table.detail ?? schemaDetail ?? (table.type === "table" ? undefined : table.type));
return {
label: table.name,
type: "table" as const,
detail: table.detail ?? (table.schema ? `${table.schema}.${table.name}` : table.type),
detail,
apply: formatTableAliasApply(applyName, alias, databaseType, keywordCase),
boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0),
dedupeKey: table.applyName || ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined,
@@ -138,7 +138,7 @@ describe("connectionStore completion assistant", () => {
it("does not replace the active connection during a cold metadata search", async () => {
const connectDb = vi.fn().mockResolvedValue("pg-1");
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "users", kind: "table", schema: "public" }],
candidates: [{ name: "users", kind: "table", schema: "public", comment: "Application users" }],
incomplete: false,
fallback_used: false,
});
@@ -161,7 +161,7 @@ describe("connectionStore completion assistant", () => {
expect(connectDb).toHaveBeenCalledOnce();
expect(store.connectedIds.has("pg-1")).toBe(true);
expect(store.activeConnectionId).toBe("already-active");
expect(tables).toEqual([{ name: "users", schema: "public", type: "table" }]);
expect(tables).toEqual([{ name: "users", schema: "public", type: "table", detail: "→ Application users" }]);
});
it("falls back to the server COMMAND catalog when COMMAND DOCS is unsupported", async () => {
@@ -357,7 +357,7 @@ describe("connectionStore completion assistant", () => {
it("returns fallback metadata when assistant table search fails", async () => {
const completionAssistantSearch = vi.fn().mockRejectedValue(new Error("assistant unavailable"));
const listTables = vi.fn().mockResolvedValue([{ name: "accounts", table_type: "BASE TABLE", comment: null }]);
const listTables = vi.fn().mockResolvedValue([{ name: "accounts", table_type: "BASE TABLE", comment: "Customer accounts" }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
@@ -376,7 +376,7 @@ describe("connectionStore completion assistant", () => {
expect(completionAssistantSearch).toHaveBeenCalledTimes(1);
expect(listTables).toHaveBeenCalledWith("pg-1", "app", "public", "acc", 20);
expect(tables).toEqual([{ name: "accounts", schema: "public", type: "table" }]);
expect(tables).toEqual([{ name: "accounts", schema: "public", type: "table", detail: "→ Customer accounts" }]);
});
it("keeps schema-qualified local table completion scoped to the selected schema", async () => {
@@ -463,8 +463,8 @@ describe("connectionStore completion assistant", () => {
it("maps global Oracle tables with safe qualification and schema priority", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [
{ name: "DEPT_DICT", kind: "table", schema: "APP", data_type: "TABLE" },
{ name: "DEPT_DICT", kind: "view", schema: "COMM", data_type: "VIEW" },
{ name: "DEPT_DICT", kind: "table", schema: "APP", data_type: "TABLE", comment: "Department dictionary" },
{ name: "DEPT_DICT", kind: "view", schema: "COMM", data_type: "VIEW", comment: "Department dictionary" },
{ name: "V_DEPT_DICT", kind: "view", schema: "SYS", data_type: "VIEW" },
{ name: "DEPT_DICT_ALIAS", kind: "table", schema: "PUBLIC", data_type: "SYNONYM" },
],
@@ -488,8 +488,8 @@ describe("connectionStore completion assistant", () => {
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "APP", parent_schema: null, global_search: true, mask: "DEPT_D" }));
expect(tables).toEqual([
expect.objectContaining({ name: "DEPT_DICT", schema: "APP", applyName: "DEPT_DICT", boost: 2400 }),
expect.objectContaining({ name: "DEPT_DICT", schema: "COMM", applyName: "COMM.DEPT_DICT", boost: 0 }),
expect.objectContaining({ name: "DEPT_DICT", schema: "APP", detail: "APP · table → Department dictionary", applyName: "DEPT_DICT", boost: 2400 }),
expect.objectContaining({ name: "DEPT_DICT", schema: "COMM", detail: "COMM · view → Department dictionary", applyName: "COMM.DEPT_DICT", boost: 0 }),
expect.objectContaining({ name: "V_DEPT_DICT", schema: "SYS", applyName: "SYS.V_DEPT_DICT", boost: -1200 }),
expect.objectContaining({ name: "DEPT_DICT_ALIAS", schema: "PUBLIC", applyName: "DEPT_DICT_ALIAS", detail: "PUBLIC · synonym", boost: 1200 }),
]);
+32 -36
View File
@@ -1951,13 +1951,23 @@ export const useConnectionStore = defineStore("connection", () => {
return supportsPackageMemberExpansion(databaseType) ? markPackageNodesExpandable(children) : children;
}
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
return tables.map((table) => ({
name: table.name,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
function completionTableDetail(comment: string | null | undefined): string | undefined {
const normalized = comment?.trim();
return normalized ? `→ ${normalized}` : undefined;
}
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string, catalog?: string): SqlCompletionTable[] {
return tables.map((table) => {
const detail = completionTableDetail(table.comment);
return {
name: table.name,
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...(detail ? { detail } : {}),
...completionStableTableType(table.table_type),
};
});
}
function completionStableTableType(tableType: string | null | undefined): Partial<Pick<SqlCompletionTable, "tableType">> {
@@ -6907,16 +6917,19 @@ export const useConnectionStore = defineStore("connection", () => {
return candidates
.filter((candidate) => candidate.kind === "table" || candidate.kind === "view")
.map((candidate) => {
const detail = completionTableDetail(candidate.comment);
const table: SqlCompletionTable = {
name: candidate.name,
schema: candidate.schema ?? undefined,
type: sqlObjectNavigationTypeFromTableType(candidate.data_type || candidate.kind),
...(detail ? { detail } : {}),
...completionStableTableType(candidate.data_type),
};
if (!withOracleMetadata) return table;
const metadataDetail = candidate.schema ? `${candidate.schema} · ${(candidate.data_type || candidate.kind).toLowerCase()}` : candidate.kind;
return {
...table,
detail: candidate.schema ? `${candidate.schema} · ${(candidate.data_type || candidate.kind).toLowerCase()}` : candidate.kind,
detail: table.detail ? `${metadataDetail} ${table.detail}` : metadataDetail,
applyName: completionCandidateApplyName(candidate.name, candidate.schema, preferredSchema),
boost: completionCandidateSchemaBoost(candidate.schema, preferredSchema),
};
@@ -7506,13 +7519,7 @@ export const useConnectionStore = defineStore("connection", () => {
} catch {
if (schema) {
const tables = await listCompletionTableMetadata(connectionId, database, schema, trimmedFilter, limit, catalog);
results = tables.map((table) => ({
name: table.name,
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
results = tableInfosToCompletionTables(tables, schema, catalog);
} else {
results = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog);
}
@@ -7527,13 +7534,7 @@ export const useConnectionStore = defineStore("connection", () => {
} else if (schema) {
try {
const tables = await listCompletionTableMetadata(connectionId, database, schema, relaxedFilter, expandedCompletionLimit(limit), catalog);
results = tables.map((table) => ({
name: table.name,
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
results = tableInfosToCompletionTables(tables, schema, catalog);
} catch {
results = [];
}
@@ -7552,13 +7553,7 @@ export const useConnectionStore = defineStore("connection", () => {
let scopedTables: SqlCompletionTable[];
if (schema) {
const tables = await listCompletionTableMetadata(connectionId, database, schema, undefined, undefined, catalog);
scopedTables = tables.map((table) => ({
name: table.name,
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
scopedTables = tableInfosToCompletionTables(tables, schema, catalog);
} else {
scopedTables = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog);
}
@@ -7575,12 +7570,7 @@ export const useConnectionStore = defineStore("connection", () => {
tables = await listCompletionTableMetadata(connectionId, database, querySchema, relaxedFilter, expandedCompletionLimit(limit), catalog);
}
if (requestRevision !== completionCacheRevision(connectionId, database)) return listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, currentSchema, catalog, options);
completionTablesCache.value[cacheKey] = tables.map((table) => ({
name: table.name,
catalog,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
completionTablesCache.value[cacheKey] = tableInfosToCompletionTables(tables, undefined, catalog);
completionTablesCache.value[cacheKey] = limit ? completionTablesCache.value[cacheKey].slice(0, limit) : completionTablesCache.value[cacheKey];
indexCompletionTables(connectionId, database, schema, completionTablesCache.value[cacheKey], catalog);
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
@@ -7609,7 +7599,13 @@ export const useConnectionStore = defineStore("connection", () => {
if (existingIndex != null) {
const existing = deduped[existingIndex];
// Loaded tree metadata can distinguish materialized views even when an older completion endpoint only reports VIEW.
deduped[existingIndex] = { ...table, ...existing, type: mergeSqlObjectNavigationType(existing.type, table.type) };
const detail = existing.detail ?? table.detail;
deduped[existingIndex] = {
...table,
...existing,
...(detail ? { detail } : {}),
type: mergeSqlObjectNavigationType(existing.type, table.type),
};
continue;
}
indexByKey.set(key, deduped.length);