perf(editor): optimize SQL autocomplete responsiveness
- Add 150ms debounce on async completion path to reduce redundant Tauri IPC calls during rapid typing - Parallelize schema table fetching with max concurrency of 5 - Separate schema list cache from table completion cache for longer-lived reuse - Return keyword completions synchronously when no backend data is needed - Lower snippet base boost for label-only matches so keywords rank above snippets when user types past snippet prefix
This commit is contained in:
1 parent
39b960c46c
commit
1d2fec4aee
3 files changed
+290
-229
No files matched your search
@@ -716,6 +716,36 @@ async function formatCurrentSql() {
|
||||
}
|
||||
|
||||
let completionEpoch = 0;
|
||||
let completionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function buildCompletionResult(
|
||||
items: ReturnType<typeof buildSqlCompletionItemsFromContext>,
|
||||
position: number,
|
||||
prefixLength: number,
|
||||
fullDoc: string,
|
||||
) {
|
||||
if (items.length === 0) return null;
|
||||
return {
|
||||
from: position - prefixLength,
|
||||
filter: false,
|
||||
options: items.map((item) =>
|
||||
(item.type === "snippet" || item.type === "function") && item.apply
|
||||
? codeMirrorSnippetCompletion(item.apply, {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
})
|
||||
: {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
},
|
||||
),
|
||||
validFor: getSqlCompletionResultValidFor(fullDoc, position),
|
||||
};
|
||||
}
|
||||
|
||||
async function provideSqlCompletions(
|
||||
currentState: import("@codemirror/state").EditorState,
|
||||
@@ -741,236 +771,241 @@ async function provideSqlCompletions(
|
||||
translations: completionTranslations.value,
|
||||
snippets: settingsStore.editorSettings.snippets,
|
||||
});
|
||||
if (items.length === 0) return null;
|
||||
return {
|
||||
from: position - completionContext.prefix.length,
|
||||
filter: false,
|
||||
options: items.map((item) =>
|
||||
(item.type === "snippet" || item.type === "function") && item.apply
|
||||
? codeMirrorSnippetCompletion(item.apply, {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
})
|
||||
: {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
},
|
||||
),
|
||||
validFor: getSqlCompletionResultValidFor(fullDoc, position),
|
||||
};
|
||||
return buildCompletionResult(items, position, completionContext.prefix.length, fullDoc);
|
||||
}
|
||||
|
||||
// Handle INSERT column list: fetch columns for the target table
|
||||
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
try {
|
||||
const insertCols = await connectionStore.listCompletionColumns(
|
||||
props.connectionId,
|
||||
props.database,
|
||||
completionContext.insertTable,
|
||||
completionContext.insertSchema,
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
if (insertCols.length > 0) {
|
||||
const insertKey = completionContext.insertSchema
|
||||
? `${completionContext.insertSchema}.${completionContext.insertTable}`
|
||||
: completionContext.insertTable;
|
||||
insertColumnsByTable.set(insertKey, insertCols);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const needsAsyncData =
|
||||
completionContext.suggestTables ||
|
||||
!!completionContext.qualifier ||
|
||||
!!completionContext.insertTable ||
|
||||
completionContext.exclusiveColumnSuggestions ||
|
||||
completionContext.referencedTables.length > 0;
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || !!completionContext.qualifier;
|
||||
let tables = shouldLoadTables
|
||||
? await connectionStore.listCompletionTables(
|
||||
props.connectionId,
|
||||
props.database,
|
||||
completionContext.qualifier || completionContext.prefix,
|
||||
MAX_COMPLETION_TABLES,
|
||||
)
|
||||
: cachedTables;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
// Fetch schemas for schema completion (only in table-suggesting context without qualifier)
|
||||
let schemaNames: string[] = [];
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
try {
|
||||
schemaNames = await api.listSchemas(props.connectionId, props.database);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (
|
||||
completionContext.qualifier &&
|
||||
tables.length === 0 &&
|
||||
(completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)
|
||||
) {
|
||||
const schemaTables = await connectionStore.listCompletionTables(
|
||||
props.connectionId,
|
||||
props.database,
|
||||
completionContext.prefix,
|
||||
MAX_COMPLETION_TABLES,
|
||||
completionContext.qualifier,
|
||||
);
|
||||
if (schemaTables.length > 0) {
|
||||
tables = schemaTables;
|
||||
qualifierIsSchema = true;
|
||||
}
|
||||
if (epoch !== completionEpoch) return null;
|
||||
}
|
||||
|
||||
// Collect referenced tables — enrich with schema from filtered table lookup
|
||||
let refs = completionContext.referencedTables.map((rt) => {
|
||||
if (!rt.schema) {
|
||||
const cached = tables.find((t) => t.name.toLowerCase() === rt.name.toLowerCase());
|
||||
if (cached && cached.schema) {
|
||||
return { ...rt, schema: cached.schema };
|
||||
}
|
||||
}
|
||||
return rt;
|
||||
});
|
||||
const unresolvedRefs = refs.filter((rt) => !rt.schema && !rt.columns);
|
||||
if (unresolvedRefs.length > 0) {
|
||||
const lookupGroups = await Promise.all(
|
||||
unresolvedRefs.map((rt) =>
|
||||
connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20),
|
||||
),
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
const lookupTables = lookupGroups.flat();
|
||||
refs = refs.map((rt) => {
|
||||
if (rt.schema || rt.columns) return rt;
|
||||
const matched = lookupTables.find((table) => table.name.toLowerCase() === rt.name.toLowerCase());
|
||||
return matched?.schema ? { ...rt, schema: matched.schema } : rt;
|
||||
if (!needsAsyncData) {
|
||||
const items = buildSqlCompletionItemsFromContext(completionContext, {
|
||||
tables: [],
|
||||
columnsByTable: new Map(),
|
||||
schemas: [],
|
||||
translations: completionTranslations.value,
|
||||
snippets: settingsStore.editorSettings.snippets,
|
||||
});
|
||||
return buildCompletionResult(items, position, completionContext.prefix.length, fullDoc);
|
||||
}
|
||||
|
||||
// If no referenced tables but qualifier exists, infer table from tables list
|
||||
if (refs.length === 0 && completionContext.qualifier) {
|
||||
const q = completionContext.qualifier.toLowerCase();
|
||||
const matched = tables.filter((t) => t.name.toLowerCase() === q || t.name.toLowerCase().endsWith("." + q));
|
||||
refs = matched.map((t) => ({ name: t.name, schema: t.schema }));
|
||||
// Cancel any pending debounced completion
|
||||
if (completionDebounceTimer) {
|
||||
clearTimeout(completionDebounceTimer);
|
||||
completionDebounceTimer = null;
|
||||
}
|
||||
|
||||
// Populate CTE columns from parsed definitions (no backend call needed)
|
||||
const cteDefs = extractCteDefinitions(fullDoc);
|
||||
for (const refTable of refs) {
|
||||
if (refTable.columns) continue; // Already has columns from CTE parsing
|
||||
const cteDef = cteDefs.find((c) => c.name.toLowerCase() === refTable.name.toLowerCase());
|
||||
if (cteDef) {
|
||||
refTable.columns = cteDef.columns;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
refs.map(async (refTable) => {
|
||||
// Skip backend fetch if columns already provided by CTE parsing
|
||||
if (refTable.columns && refTable.columns.length > 0) return;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cachedColumnsByTable.has(cacheKey)) {
|
||||
// Debounce the full async flow and return the promise to CodeMirror.
|
||||
// This prevents wasted backend calls during rapid typing while still
|
||||
// showing table/column names in the first popup.
|
||||
return new Promise<ReturnType<typeof buildCompletionResult>>((resolve) => {
|
||||
completionDebounceTimer = setTimeout(async () => {
|
||||
completionDebounceTimer = null;
|
||||
if (epoch !== completionEpoch) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const columns = await connectionStore.listCompletionColumns(
|
||||
props.connectionId!,
|
||||
props.database!,
|
||||
refTable.name,
|
||||
refTable.schema,
|
||||
);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
} catch (e) {
|
||||
console.error(`[DBX] Failed to load columns for ${cacheKey}:`, e);
|
||||
const result = await performAsyncCompletionWithResult(epoch, completionContext, fullDoc, position);
|
||||
resolve(result);
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
// Build columnsByTable — from cache or CTE definitions
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (insertColumnsByTable.size > 0) {
|
||||
for (const [key, cols] of insertColumnsByTable.entries()) {
|
||||
columnsByTable.set(key, cols);
|
||||
}
|
||||
} else {
|
||||
for (const refTable of refs) {
|
||||
// Use CTE columns if available
|
||||
if (refTable.columns && refTable.columns.length > 0) {
|
||||
const key = refTable.name;
|
||||
columnsByTable.set(
|
||||
key,
|
||||
refTable.columns.map((name) => ({
|
||||
name,
|
||||
table: refTable.name,
|
||||
dataType: undefined,
|
||||
})),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
const cached = cachedColumnsByTable.get(cacheKey);
|
||||
if (cached) {
|
||||
columnsByTable.set(cacheKey, cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveContext = qualifierIsSchema
|
||||
? {
|
||||
...completionContext,
|
||||
qualifier: undefined,
|
||||
suggestTables: true,
|
||||
suggestColumns: false,
|
||||
exclusiveColumnSuggestions: false,
|
||||
}
|
||||
: completionContext;
|
||||
|
||||
const items = buildSqlCompletionItemsFromContext(effectiveContext, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
schemas: schemaNames,
|
||||
translations: completionTranslations.value,
|
||||
snippets: settingsStore.editorSettings.snippets,
|
||||
}, 150);
|
||||
});
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return {
|
||||
from: position - completionContext.prefix.length,
|
||||
filter: false,
|
||||
options: items.map((item) =>
|
||||
(item.type === "snippet" || item.type === "function") && item.apply
|
||||
? codeMirrorSnippetCompletion(item.apply, {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
})
|
||||
: {
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
detail: item.detail,
|
||||
boost: item.boost,
|
||||
},
|
||||
),
|
||||
validFor: getSqlCompletionResultValidFor(fullDoc, position),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function performAsyncCompletionWithResult(
|
||||
epoch: number,
|
||||
completionContext: ReturnType<typeof getSqlCompletionContext>,
|
||||
fullDoc: string,
|
||||
position: number,
|
||||
) {
|
||||
// Handle INSERT column list: fetch columns for the target table
|
||||
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
try {
|
||||
const insertCols = await connectionStore.listCompletionColumns(
|
||||
props.connectionId!,
|
||||
props.database!,
|
||||
completionContext.insertTable,
|
||||
completionContext.insertSchema,
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
if (insertCols.length > 0) {
|
||||
const insertKey = completionContext.insertSchema
|
||||
? `${completionContext.insertSchema}.${completionContext.insertTable}`
|
||||
: completionContext.insertTable;
|
||||
insertColumnsByTable.set(insertKey, insertCols);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || !!completionContext.qualifier;
|
||||
let tables = shouldLoadTables
|
||||
? await connectionStore.listCompletionTables(
|
||||
props.connectionId!,
|
||||
props.database!,
|
||||
completionContext.qualifier || completionContext.prefix,
|
||||
MAX_COMPLETION_TABLES,
|
||||
)
|
||||
: cachedTables;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
// Fetch schemas for schema completion
|
||||
let schemaNames: string[] = [];
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
try {
|
||||
schemaNames = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (
|
||||
completionContext.qualifier &&
|
||||
tables.length === 0 &&
|
||||
(completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)
|
||||
) {
|
||||
const schemaTables = await connectionStore.listCompletionTables(
|
||||
props.connectionId!,
|
||||
props.database!,
|
||||
completionContext.prefix,
|
||||
MAX_COMPLETION_TABLES,
|
||||
completionContext.qualifier,
|
||||
);
|
||||
if (schemaTables.length > 0) {
|
||||
tables = schemaTables;
|
||||
qualifierIsSchema = true;
|
||||
}
|
||||
if (epoch !== completionEpoch) return null;
|
||||
}
|
||||
|
||||
// Collect referenced tables — enrich with schema from filtered table lookup
|
||||
let refs = completionContext.referencedTables.map((rt) => {
|
||||
if (!rt.schema) {
|
||||
const cached = tables.find((t) => t.name.toLowerCase() === rt.name.toLowerCase());
|
||||
if (cached && cached.schema) {
|
||||
return { ...rt, schema: cached.schema };
|
||||
}
|
||||
}
|
||||
return rt;
|
||||
});
|
||||
const unresolvedRefs = refs.filter((rt) => !rt.schema && !rt.columns);
|
||||
if (unresolvedRefs.length > 0) {
|
||||
const lookupGroups = await Promise.all(
|
||||
unresolvedRefs.map((rt) =>
|
||||
connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20),
|
||||
),
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
const lookupTables = lookupGroups.flat();
|
||||
refs = refs.map((rt) => {
|
||||
if (rt.schema || rt.columns) 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) {
|
||||
const q = completionContext.qualifier.toLowerCase();
|
||||
const matched = tables.filter((t) => t.name.toLowerCase() === q || t.name.toLowerCase().endsWith("." + q));
|
||||
refs = matched.map((t) => ({ name: t.name, schema: t.schema }));
|
||||
}
|
||||
|
||||
// Populate CTE columns from parsed definitions
|
||||
const cteDefs = extractCteDefinitions(fullDoc);
|
||||
for (const refTable of refs) {
|
||||
if (refTable.columns) continue;
|
||||
const cteDef = cteDefs.find((c) => c.name.toLowerCase() === refTable.name.toLowerCase());
|
||||
if (cteDef) {
|
||||
refTable.columns = cteDef.columns;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
refs.map(async (refTable) => {
|
||||
if (refTable.columns && refTable.columns.length > 0) return;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cachedColumnsByTable.has(cacheKey)) return;
|
||||
try {
|
||||
const columns = await connectionStore.listCompletionColumns(
|
||||
props.connectionId!,
|
||||
props.database!,
|
||||
refTable.name,
|
||||
refTable.schema,
|
||||
);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
} catch (e) {
|
||||
console.error(`[DBX] Failed to load columns for ${cacheKey}:`, e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
// Build columnsByTable — from cache or CTE definitions
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (insertColumnsByTable.size > 0) {
|
||||
for (const [key, cols] of insertColumnsByTable.entries()) {
|
||||
columnsByTable.set(key, cols);
|
||||
}
|
||||
} else {
|
||||
for (const refTable of refs) {
|
||||
if (refTable.columns && refTable.columns.length > 0) {
|
||||
const key = refTable.name;
|
||||
columnsByTable.set(
|
||||
key,
|
||||
refTable.columns.map((name) => ({
|
||||
name,
|
||||
table: refTable.name,
|
||||
dataType: undefined,
|
||||
})),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
const cached = cachedColumnsByTable.get(cacheKey);
|
||||
if (cached) {
|
||||
columnsByTable.set(cacheKey, cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveContext = qualifierIsSchema
|
||||
? {
|
||||
...completionContext,
|
||||
qualifier: undefined,
|
||||
suggestTables: true,
|
||||
suggestColumns: false,
|
||||
exclusiveColumnSuggestions: false,
|
||||
}
|
||||
: completionContext;
|
||||
|
||||
const items = buildSqlCompletionItemsFromContext(effectiveContext, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
schemas: schemaNames,
|
||||
translations: completionTranslations.value,
|
||||
snippets: settingsStore.editorSettings.snippets,
|
||||
});
|
||||
|
||||
return buildCompletionResult(items, position, completionContext.prefix.length, fullDoc);
|
||||
}
|
||||
|
||||
async function refreshCompletionCache() {
|
||||
cachedTables = [];
|
||||
cachedColumnsByTable.clear();
|
||||
|
||||
@@ -1942,12 +1942,17 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[]): SqlCompletio
|
||||
.map((snippet) => {
|
||||
const boostByPrefix = computeBoost(snippet.prefix, prefix);
|
||||
const boostByLabel = computeBoost(snippet.label, prefix);
|
||||
const matchesByPrefix = matchesPrefix(snippet.prefix, prefix);
|
||||
// When the user types past the snippet prefix (e.g. "sele" vs prefix "sel"),
|
||||
// they are likely typing the actual keyword — reduce the base boost so
|
||||
// the real keyword can rank higher.
|
||||
const baseBoost = matchesByPrefix ? 4000 : 0;
|
||||
return {
|
||||
label: snippet.label,
|
||||
type: "snippet" as const,
|
||||
detail: snippet.body,
|
||||
apply: snippet.body,
|
||||
boost: Math.max(boostByPrefix, boostByLabel) + 4000,
|
||||
boost: Math.max(boostByPrefix, boostByLabel) + baseBoost,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const newConnectionGroupId = ref<string | null>(null);
|
||||
const completionTablesCache = ref<Record<string, SqlCompletionTable[]>>({});
|
||||
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
|
||||
const schemaListCache = ref<Record<string, string[]>>({});
|
||||
const transferSource = ref<{ connectionId: string; database: string } | null>(null);
|
||||
const schemaDiffSource = ref<{ connectionId: string; database: string; schema?: string } | null>(null);
|
||||
const dataCompareSource = ref<{
|
||||
@@ -515,6 +516,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
for (const key of Object.keys(completionColumnsCache.value)) {
|
||||
if (key.startsWith(cachePrefix)) delete completionColumnsCache.value[key];
|
||||
}
|
||||
for (const key of Object.keys(schemaListCache.value)) {
|
||||
if (key.startsWith(cachePrefix)) delete schemaListCache.value[key];
|
||||
}
|
||||
}
|
||||
|
||||
async function removeConnection(id: string) {
|
||||
@@ -1358,6 +1362,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function listCompletionSchemas(connectionId: string, database: string): Promise<string[]> {
|
||||
const cacheKey = `${connectionId}:${database}`;
|
||||
if (schemaListCache.value[cacheKey]) {
|
||||
return schemaListCache.value[cacheKey];
|
||||
}
|
||||
const schemas = await api.listSchemas(connectionId, database);
|
||||
schemaListCache.value[cacheKey] = schemas;
|
||||
return schemas;
|
||||
}
|
||||
|
||||
async function listCompletionTables(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
@@ -1374,25 +1388,31 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
|
||||
if (isSchemaAwareDatabase(connectionId)) {
|
||||
const schemas = schema ? [schema] : await api.listSchemas(connectionId, database);
|
||||
const schemas = schema ? [schema] : await listCompletionSchemas(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 */
|
||||
const batchSize = 5;
|
||||
const results: SqlCompletionTable[] = [];
|
||||
for (let i = 0; i < schemas.length && results.length < (limit ?? Infinity); i += batchSize) {
|
||||
const batch = schemas.slice(i, i + batchSize);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (s) => {
|
||||
try {
|
||||
const tables = await api.listTables(connectionId, database, s, normalizedFilter, limit);
|
||||
return tables.map((table) => ({
|
||||
name: table.name,
|
||||
schema: s,
|
||||
type: table.table_type === "VIEW" ? ("view" as const) : ("table" as const),
|
||||
})) as SqlCompletionTable[];
|
||||
} catch {
|
||||
return [] as SqlCompletionTable[];
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const group of batchResults) {
|
||||
results.push(...group);
|
||||
}
|
||||
}
|
||||
const limitedTables = limit ? results.slice(0, limit) : results;
|
||||
completionTablesCache.value[cacheKey] = limitedTables;
|
||||
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
|
||||
return completionTablesCache.value[cacheKey];
|
||||
@@ -1830,6 +1850,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadTriggers,
|
||||
listCompletionTables,
|
||||
listCompletionColumns,
|
||||
listCompletionSchemas,
|
||||
exportConnectionsToFile,
|
||||
readImportFile,
|
||||
importConnectionsFromFile,
|
||||
|
||||
Reference in new issue
Block a user