fix: filter SQL completion tables

This commit is contained in:
t8y2
2026-05-13 11:01:59 +08:00
parent b0a808bc14
commit 047e8fc2f1
11 changed files with 174 additions and 41 deletions
+33 -12
View File
@@ -39,6 +39,7 @@ const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const MIN_FONT_SIZE = 10;
const MAX_FONT_SIZE = 24;
const MAX_COMPLETION_TABLES = 200;
let editorViewModule: typeof import("@codemirror/view") | null = null;
let fontThemeComp: import("@codemirror/state").Compartment | null = null;
let codeMirrorTheme: import("@codemirror/state").Compartment | null = null;
@@ -126,9 +127,17 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
try {
const fullDoc = currentState.doc.toString();
const completionContext = getSqlCompletionContext(fullDoc, position);
const tables = await connectionStore.listCompletionTables(props.connectionId, props.database);
const shouldLoadTables = completionContext.suggestTables || !!completionContext.qualifier;
const tables = shouldLoadTables
? await connectionStore.listCompletionTables(
props.connectionId,
props.database,
completionContext.qualifier || completionContext.prefix,
MAX_COMPLETION_TABLES,
)
: cachedTables;
// Collect referenced tables — enrich with schema from cached tables list
// Collect referenced tables — enrich with schema from filtered table lookup
let refs = completionContext.referencedTables.map((rt) => {
// If no schema, look it up in the cached tables
if (!rt.schema) {
@@ -139,6 +148,20 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
}
return rt;
});
const unresolvedRefs = refs.filter((rt) => !rt.schema);
if (unresolvedRefs.length > 0) {
const lookupGroups = await Promise.all(
unresolvedRefs.map((rt) =>
connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20),
),
);
const lookupTables = lookupGroups.flat();
refs = refs.map((rt) => {
if (rt.schema) return rt;
const matched = lookupTables.find((table) => table.name.toLowerCase() === rt.name.toLowerCase());
return matched?.schema ? { ...rt, schema: matched.schema } : rt;
});
}
// If no referenced tables but qualifier exists, infer table from tables list
if (refs.length === 0 && completionContext.qualifier) {
@@ -200,13 +223,7 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
}
async function refreshCompletionCache() {
if (!props.connectionId || !props.database) return;
try {
cachedTables = await connectionStore.listCompletionTables(props.connectionId, props.database);
} catch {
cachedTables = [];
}
// Clear column cache when connection/database changes
cachedTables = [];
cachedColumnsByTable.clear();
}
@@ -370,7 +387,12 @@ onMounted(async () => {
try {
// Ensure table cache is populated
if (cachedTables.length === 0) {
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, props.database!);
cachedTables = await connectionStore.listCompletionTables(
props.connectionId!,
props.database!,
identifier,
MAX_COMPLETION_TABLES,
);
}
// 1. Check if it's a table name
@@ -463,8 +485,7 @@ onMounted(async () => {
view.value = new EditorView({ state, parent: editorRef.value });
// Fetch completion data
refreshCompletionCache();
cachedTables = [];
});
watch(
+8 -2
View File
@@ -172,8 +172,14 @@ export async function listSchemas(connectionId: string, database: string): Promi
return get(`/api/schema/schemas?${qs({ connection_id: connectionId, database })}`);
}
export async function listTables(connectionId: string, database: string, schema: string): Promise<TableInfo[]> {
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema })}`);
export async function listTables(
connectionId: string,
database: string,
schema: string,
filter?: string,
limit?: number,
): Promise<TableInfo[]> {
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema, filter, limit })}`);
}
export async function listObjects(connectionId: string, database: string, schema: string): Promise<ObjectInfo[]> {
+2
View File
@@ -133,6 +133,7 @@ const SQL_KEYWORDS = [
const TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "into", "table", "describe", "explain", "apply"]);
const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "full", "natural"]);
const MAX_TABLE_COMPLETION_ITEMS = 200;
export interface SqlCompletionTable {
name: string;
@@ -463,6 +464,7 @@ function unquoteIdentifier(value: string): string {
function buildTableItems(prefix: string, tables: SqlCompletionTable[]): SqlCompletionItem[] {
return tables
.filter((table) => matchesPrefix(table.name, prefix))
.slice(0, MAX_TABLE_COMPLETION_ITEMS)
.map((table) => ({
label: table.name,
type: "table" as const,
+8 -2
View File
@@ -138,8 +138,14 @@ 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): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema });
export async function listTables(
connectionId: string,
database: string,
schema: string,
filter?: string,
limit?: number,
): Promise<TableInfo[]> {
return invoke("list_tables", { connectionId, database, schema, filter, limit });
}
export async function listObjects(connectionId: string, database: string, schema: string): Promise<ObjectInfo[]> {
+32 -3
View File
@@ -990,8 +990,14 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function listCompletionTables(connectionId: string, database: string): Promise<SqlCompletionTable[]> {
const cacheKey = `${connectionId}:${database}`;
async function listCompletionTables(
connectionId: string,
database: string,
filter = "",
limit?: number,
): Promise<SqlCompletionTable[]> {
const normalizedFilter = filter.trim().toLowerCase();
const cacheKey = `${connectionId}:${database}:${normalizedFilter}:${limit ?? ""}`;
if (completionTablesCache.value[cacheKey]) {
return completionTablesCache.value[cacheKey];
}
@@ -1000,6 +1006,29 @@ export const useConnectionStore = defineStore("connection", () => {
if (isSchemaAwareDatabase(connectionId)) {
const schemas = await api.listSchemas(connectionId, database);
if (normalizedFilter || limit) {
const limitedTables: SqlCompletionTable[] = [];
for (const schema of schemas) {
try {
const remaining = limit ? Math.max(limit - limitedTables.length, 0) : undefined;
if (remaining === 0) break;
const tables = await api.listTables(connectionId, database, schema, normalizedFilter, remaining);
limitedTables.push(
...tables.map((table) => ({
name: table.name,
schema,
type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const),
})),
);
} catch {
/* ignore schema failures */
}
}
completionTablesCache.value[cacheKey] = limitedTables;
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
return completionTablesCache.value[cacheKey];
}
const tableGroups = await Promise.all(
schemas.map(async (schema) => {
try {
@@ -1019,7 +1048,7 @@ export const useConnectionStore = defineStore("connection", () => {
return completionTablesCache.value[cacheKey];
}
const tables = await api.listTables(connectionId, database, database);
const tables = await api.listTables(connectionId, database, database, normalizedFilter, limit);
completionTablesCache.value[cacheKey] = tables.map((table) => ({
name: table.name,
type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const),