feat(completion): show table metadata comments in SQL completions
This commit is contained in:
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 } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user