feat(completion): cache foreign key metadata

This commit is contained in:
t8y2 committed 2026-06-12 19:27:16 +08:00
1 parent 60632ccfe3
commit fa4dbb9104
4 files changed
+184 -36

No files matched your search

+102 -10
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { uuid } from "@/lib/utils";
import { ref, computed, watch } from "vue";
import type { ColumnInfo, ConnectionConfig, ObjectInfo, SidebarLayout, TreeNode } from "@/types/database";
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SidebarLayout, TreeNode } from "@/types/database";
import { applyPinnedTreeNodeState, orderPinnedFirst } from "@/lib/pinnedItems";
import {
reconcileLayout,
@@ -17,7 +17,7 @@ import {
reorderEntry as reorderEntryOp,
type DropPosition,
} from "@/lib/sidebarLayout";
import type { SqlCompletionColumn, SqlCompletionObject, SqlCompletionTable } from "@/lib/sqlCompletion";
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sqlCompletion";
import * as api from "@/lib/api";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/databaseCapabilities";
@@ -103,12 +103,14 @@ export const useConnectionStore = defineStore("connection", () => {
const completionTablesCache = ref<Record<string, SqlCompletionTable[]>>({});
const completionObjectsCache = ref<Record<string, SqlCompletionObject[]>>({});
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
const completionForeignKeysCache = ref<Record<string, ForeignKeyInfo[]>>({});
const completionDatabasesCache = ref<Record<string, string[]>>({});
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
const schemaListCache = ref<Record<string, string[]>>({});
const completionTableIndex = new Map<string, { touched: number; tables: SqlCompletionTable[] }>();
const completionObjectIndex = new Map<string, { touched: number; objects: SqlCompletionObject[] }>();
const completionColumnIndex = new Map<string, { touched: number; columns: SqlCompletionColumn[] }>();
const completionForeignKeyIndex = new Map<string, { touched: number; foreignKeys: SqlCompletionForeignKey[] }>();
const completionInFlight = new Map<string, Promise<unknown>>();
const transferSource = ref<{ connectionId: string; database: string } | null>(null);
const schemaDiffSource = ref<{ connectionId: string; database: string; schema?: string } | null>(null);
@@ -516,6 +518,9 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of Object.keys(completionColumnsCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete completionColumnsCache.value[key];
}
for (const key of Object.keys(completionForeignKeysCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete completionForeignKeysCache.value[key];
}
for (const key of Object.keys(schemaListCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete schemaListCache.value[key];
}
@@ -531,6 +536,9 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of completionColumnIndex.keys()) {
if (key.startsWith(cachePrefix)) completionColumnIndex.delete(key);
}
for (const key of completionForeignKeyIndex.keys()) {
if (key.startsWith(cachePrefix)) completionForeignKeyIndex.delete(key);
}
for (const key of completionInFlight.keys()) {
if (key.startsWith(cachePrefix)) completionInFlight.delete(key);
}
@@ -1368,6 +1376,10 @@ export const useConnectionStore = defineStore("connection", () => {
}
const querySchema = metadataQuerySchema(connectionId, database, schema);
const fkeys = await api.listForeignKeys(connectionId, database, querySchema, table);
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`;
completionForeignKeysCache.value[cacheKey] = fkeys;
evictOldestCacheEntries(completionForeignKeysCache.value, COMPLETION_CACHE_MAX);
indexCompletionForeignKeys(connectionId, database, table, schema, sqlCompletionForeignKeys(fkeys));
setChildren(
node,
fkeys.map((fk) => ({
@@ -1561,6 +1573,10 @@ export const useConnectionStore = defineStore("connection", () => {
return `${completionScopeKey(connectionId, database, schema)}:${table.toLowerCase()}`;
}
function completionForeignKeysKey(connectionId: string, database: string, table: string, schema?: string): string {
return `${completionScopeKey(connectionId, database, schema)}:${table.toLowerCase()}:fkeys`;
}
function touchCompletionIndex<T>(index: Map<string, { touched: number } & T>, key: string, value: T, max = COMPLETION_CACHE_MAX) {
index.set(key, { ...value, touched: Date.now() });
if (index.size <= max) return;
@@ -1578,6 +1594,32 @@ export const useConnectionStore = defineStore("connection", () => {
return promise;
}
function completionNameSegments(name: string): string[] {
return name
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.split(/[\s_.:-]+/)
.map((segment) => segment.trim().toLowerCase())
.filter(Boolean);
}
function completionNameAcronym(name: string): string {
return completionNameSegments(name)
.map((segment) => segment[0])
.join("");
}
function orderedSubsequenceScore(text: string, filter: string): number {
let index = 0;
let gaps = 0;
for (const ch of filter) {
const found = text.indexOf(ch, index);
if (found < 0) return -1;
gaps += found - index;
index = found + 1;
}
return 1_000 - gaps - text.length;
}
function tableMatchScore(table: SqlCompletionTable, filter: string, preferredSchema?: string): number {
const text = table.name.toLowerCase();
const schema = table.schema?.toLowerCase();
@@ -1585,15 +1627,16 @@ export const useConnectionStore = defineStore("connection", () => {
let score = schema && preferredSchema && schema === preferredSchema.toLowerCase() ? 10_000 : 0;
if (!normalized) return score;
if (text === normalized) return score + 9_000 - text.length;
if (text.startsWith(normalized)) return score + 7_000 - text.length;
if (text.startsWith(normalized)) return score + 7_500 - text.length;
const segments = completionNameSegments(table.name);
if (segments.some((segment) => segment.startsWith(normalized))) return score + 7_200 - text.length;
const acronym = completionNameAcronym(table.name);
if (acronym === normalized) return score + 7_100 - text.length;
if (acronym.startsWith(normalized)) return score + 6_900 - text.length;
if (normalized.length <= segments.length && segments.every((segment, index) => segment.startsWith(normalized[index] ?? ""))) return score + 6_700 - text.length;
if (text.includes(normalized)) return score + 4_000 - text.length;
let index = 0;
for (const ch of normalized) {
index = text.indexOf(ch, index);
if (index < 0) return -1;
index++;
}
return score + 1_000 - text.length;
const subsequenceScore = orderedSubsequenceScore(text, normalized);
return subsequenceScore < 0 ? -1 : score + subsequenceScore;
}
function objectMatchScore(object: SqlCompletionObject, filter: string, preferredSchema?: string): number {
@@ -1641,6 +1684,22 @@ export const useConnectionStore = defineStore("connection", () => {
});
}
function sqlCompletionForeignKeys(foreignKeys: ForeignKeyInfo[]): SqlCompletionForeignKey[] {
return foreignKeys.map((foreignKey) => ({
name: foreignKey.name,
column: foreignKey.column,
ref_schema: foreignKey.ref_schema,
ref_table: foreignKey.ref_table,
ref_column: foreignKey.ref_column,
}));
}
function indexCompletionForeignKeys(connectionId: string, database: string, table: string, schema: string | undefined, foreignKeys: SqlCompletionForeignKey[]) {
touchCompletionIndex(completionForeignKeyIndex, completionForeignKeysKey(connectionId, database, table, schema), {
foreignKeys,
});
}
function lookupLocalCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string): SqlCompletionTable[] {
const allScopes = [...completionTableIndex.entries()].filter(([key]) => key.startsWith(`${connectionId}:${database}:`)).map(([, entry]) => entry);
const preferred = schema ? completionTableIndex.get(completionScopeKey(connectionId, database, schema)) : undefined;
@@ -1687,6 +1746,10 @@ export const useConnectionStore = defineStore("connection", () => {
return completionColumnIndex.get(completionColumnsKey(connectionId, database, table, schema))?.columns ?? [];
}
function lookupLocalCompletionForeignKeys(connectionId: string, database: string, table: string, schema?: string): SqlCompletionForeignKey[] {
return completionForeignKeyIndex.get(completionForeignKeysKey(connectionId, database, table, schema))?.foreignKeys ?? [];
}
function databaseNamesFromTree(connectionId: string): string[] {
const node = findNode(treeNodes.value, connectionId);
if (!node?.children) return [];
@@ -1973,6 +2036,28 @@ export const useConnectionStore = defineStore("connection", () => {
return columns;
}
async function listCompletionForeignKeys(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionForeignKey[]> {
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(getConfig(connectionId)) && !schema) {
return [];
}
const metadataCapabilities = getTableMetadataCapabilities(effectiveDatabaseTypeForConnection(getConfig(connectionId)));
if (!metadataCapabilities.foreignKeys) return [];
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`;
if (!completionForeignKeysCache.value[cacheKey]) {
await withCompletionInFlight(`${cacheKey}:fkeys`, async () => {
await ensureConnected(connectionId);
const querySchema = metadataQuerySchema(connectionId, database, schema);
completionForeignKeysCache.value[cacheKey] = await api.listForeignKeys(connectionId, database, querySchema, table);
evictOldestCacheEntries(completionForeignKeysCache.value, COMPLETION_CACHE_MAX);
});
}
const foreignKeys = sqlCompletionForeignKeys(completionForeignKeysCache.value[cacheKey]);
indexCompletionForeignKeys(connectionId, database, table, schema, foreignKeys);
return foreignKeys;
}
function refreshCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string): Promise<SqlCompletionTable[]> {
return listCompletionTables(connectionId, database, filter, limit, schema);
}
@@ -1993,6 +2078,10 @@ export const useConnectionStore = defineStore("connection", () => {
return listCompletionColumns(connectionId, database, table, schema);
}
function refreshCompletionForeignKeys(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionForeignKey[]> {
return listCompletionForeignKeys(connectionId, database, table, schema);
}
function findNode(nodes: TreeNode[], id: string): TreeNode | null {
for (const node of nodes) {
if (node.id === id) return node;
@@ -2485,16 +2574,19 @@ export const useConnectionStore = defineStore("connection", () => {
listCompletionTables,
listCompletionObjects,
listCompletionColumns,
listCompletionForeignKeys,
listCompletionSchemas,
listCompletionDatabases,
lookupLocalCompletionTables,
lookupLocalCompletionObjects,
lookupLocalCompletionColumns,
lookupLocalCompletionForeignKeys,
lookupLocalCompletionSchemas,
lookupLocalCompletionDatabases,
refreshCompletionTables,
refreshCompletionObjects,
refreshCompletionColumns,
refreshCompletionForeignKeys,
refreshCompletionSchemas,
refreshCompletionDatabases,
listElasticsearchCompletionIndices,