mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 02:40:11 +00:00
feat(completion): suggest database qualifiers
This commit is contained in:
@@ -34,6 +34,7 @@ import { clampEditorFontSize, createEditorZoomCommitScheduler, fontSizeFromGestu
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/shortcutRegistry";
|
||||
import { trimmedSelectionLayer } from "@/lib/codemirrorTrimmedSelectionLayer";
|
||||
import { selectionMatchOccurrences } from "@/lib/codemirrorSelectionMatches";
|
||||
import { isSchemaAware, isSingleDatabase } from "@/lib/databaseFeatureSupport";
|
||||
import * as api from "@/lib/api";
|
||||
import { areSqlSemanticDiagnosticsEqual, buildSqlParserErrorDiagnostic, buildSqlSemanticDiagnostics, shouldRunSqlSemanticDiagnostics, type SqlSemanticDiagnostic } from "@/lib/sqlSemanticDiagnostics";
|
||||
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject } from "@/lib/sqlCompletion";
|
||||
@@ -416,10 +417,24 @@ function completionCacheKey(table: { name: string; schema?: string | null }) {
|
||||
return schema ? `${schema}.${table.name}` : table.name;
|
||||
}
|
||||
|
||||
function supportsDatabaseQualifierCompletion(): boolean {
|
||||
return !!props.databaseType && !isSchemaAware(props.databaseType) && !isSingleDatabase(props.databaseType);
|
||||
}
|
||||
|
||||
function completionMetadataTarget(table: { name: string; schema?: string | null }): { database: string; schema?: string } | null {
|
||||
if (props.database == null) return null;
|
||||
if (supportsDatabaseQualifierCompletion() && table.schema) {
|
||||
return { database: table.schema };
|
||||
}
|
||||
return { database: props.database, schema: table.schema ?? props.schema };
|
||||
}
|
||||
|
||||
async function ensureColumnsForTable(table: { name: string; schema?: string | null }) {
|
||||
const cacheKey = completionCacheKey(table);
|
||||
if (cachedColumnsByTable.has(cacheKey) || !props.connectionId || props.database == null) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId, props.database, table.name, table.schema ?? props.schema);
|
||||
const target = completionMetadataTarget(table);
|
||||
if (!target) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId, target.database, table.name, target.schema);
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
}
|
||||
@@ -427,9 +442,11 @@ async function ensureColumnsForTable(table: { name: string; schema?: string | nu
|
||||
async function ensureForeignKeysForTable(table: { name: string; schema?: string | null }) {
|
||||
const cacheKey = completionCacheKey(table);
|
||||
if (cachedForeignKeysByTable.has(cacheKey) || !props.connectionId || props.database == null) return;
|
||||
const querySchema = table.schema ?? props.schema ?? props.database;
|
||||
const target = completionMetadataTarget(table);
|
||||
if (!target) return;
|
||||
const querySchema = target.schema ?? target.database;
|
||||
try {
|
||||
const foreignKeys = await api.listForeignKeys(props.connectionId, props.database, querySchema, table.name);
|
||||
const foreignKeys = await api.listForeignKeys(props.connectionId, target.database, querySchema, table.name);
|
||||
cachedForeignKeysByTable.set(
|
||||
cacheKey,
|
||||
foreignKeys.map((foreignKey: ForeignKeyInfo) => ({
|
||||
@@ -801,6 +818,22 @@ function buildCompletionResult(items: QueryCompletionItem[], from: number, valid
|
||||
};
|
||||
}
|
||||
|
||||
function findExactName(names: string[], value: string): string | undefined {
|
||||
return names.find((name) => name.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
function mergeCompletionQualifierNames(primary: string[], secondary: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const merged: string[] = [];
|
||||
for (const name of [...primary, ...secondary]) {
|
||||
const key = name.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(name);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function completionOptionForItem(item: QueryCompletionItem) {
|
||||
const record = () => {
|
||||
recordCompletionSelection(item.label, item.type);
|
||||
@@ -949,17 +982,21 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
|
||||
|
||||
function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number) {
|
||||
if (!props.connectionId || props.database == null) return null;
|
||||
const databaseNames = supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable ? connectionStore.lookupLocalCompletionDatabases(props.connectionId, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES) : [];
|
||||
const qualifierDatabase = completionContext.qualifier ? findExactName(databaseNames, completionContext.qualifier) : undefined;
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
const tableLookupSchema = completionContext.qualifier && completionContext.suggestTables ? completionContext.qualifier : props.schema;
|
||||
const tableLookupDatabase = qualifierDatabase ?? props.database;
|
||||
const tableLookupSchema = qualifierDatabase ? undefined : completionContext.qualifier && completionContext.suggestTables ? completionContext.qualifier : props.schema;
|
||||
const tableLookupFilter = completionContext.qualifier && completionContext.suggestTables ? completionContext.prefix : completionContext.qualifier || completionContext.prefix;
|
||||
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) : cachedTables;
|
||||
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) : cachedTables;
|
||||
|
||||
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
|
||||
const completionObjects = shouldLoadObjects
|
||||
? connectionStore.lookupLocalCompletionObjects(props.connectionId, props.database, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier && !completionContext.exclusiveColumnSuggestions ? completionContext.qualifier : props.schema)
|
||||
: cachedCompletionObjects;
|
||||
|
||||
const schemaNames = completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable ? connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES) : [];
|
||||
const schemaNames =
|
||||
completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable ? mergeCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames) : [];
|
||||
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
@@ -986,7 +1023,8 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
columnsByTable.set(cacheKey, cached);
|
||||
continue;
|
||||
}
|
||||
const localColumns = connectionStore.lookupLocalCompletionColumns(props.connectionId, props.database, refTable.name, refTable.schema ?? props.schema);
|
||||
const target = completionMetadataTarget(refTable);
|
||||
const localColumns = target ? connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, refTable.name, target.schema) : [];
|
||||
if (localColumns.length > 0) {
|
||||
columnsByTable.set(cacheKey, localColumns);
|
||||
}
|
||||
@@ -1034,6 +1072,9 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
}
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
void connectionStore.refreshCompletionSchemas(connectionId, database).catch(() => {});
|
||||
if (supportsDatabaseQualifierCompletion()) {
|
||||
void connectionStore.refreshCompletionDatabases(connectionId).catch(() => {});
|
||||
}
|
||||
}
|
||||
if (completionContext.insertTable) {
|
||||
const insertTable = completionContext.insertTable;
|
||||
@@ -1049,8 +1090,10 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
if (refTable.columns && refTable.columns.length > 0) continue;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cachedColumnsByTable.has(cacheKey)) continue;
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, database, refTable.name, refTable.schema ?? props.schema)
|
||||
.refreshCompletionColumns(connectionId, target.database, refTable.name, target.schema)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(cacheKey, columns);
|
||||
})
|
||||
@@ -1088,7 +1131,20 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
}
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
let tables = shouldLoadTables ? await connectionStore.listCompletionTables(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema) : cachedTables;
|
||||
let databaseNames: string[] = [];
|
||||
if (supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable) {
|
||||
try {
|
||||
databaseNames = await connectionStore.listCompletionDatabases(props.connectionId!);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
databaseNames = [];
|
||||
}
|
||||
}
|
||||
const qualifierDatabase = completionContext.qualifier ? findExactName(databaseNames, completionContext.qualifier) : undefined;
|
||||
const tableLookupDatabase = qualifierDatabase ?? props.database!;
|
||||
const tableLookupSchema = qualifierDatabase ? undefined : props.schema;
|
||||
const tableLookupFilter = completionContext.qualifier && !qualifierDatabase ? completionContext.qualifier : completionContext.prefix;
|
||||
let tables = shouldLoadTables ? await connectionStore.listCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) : cachedTables;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
|
||||
@@ -1108,16 +1164,17 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
let schemaNames: string[] = [];
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
try {
|
||||
schemaNames = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
const schemas = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
schemaNames = mergeCompletionQualifierNames(schemas, databaseNames);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
// ignore
|
||||
schemaNames = databaseNames;
|
||||
}
|
||||
}
|
||||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (completionContext.qualifier && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
if (completionContext.qualifier && !qualifierDatabase && !isReferencedTableQualifier(completionContext) && 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;
|
||||
@@ -1171,7 +1228,9 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
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 ?? props.schema);
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId!, target.database, refTable.name, target.schema);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
|
||||
@@ -102,6 +102,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const completionTablesCache = ref<Record<string, SqlCompletionTable[]>>({});
|
||||
const completionObjectsCache = ref<Record<string, SqlCompletionObject[]>>({});
|
||||
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
|
||||
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[] }>();
|
||||
@@ -1611,10 +1612,50 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function lookupLocalCompletionDatabases(connectionId: string, filter = "", limit = 50): string[] {
|
||||
const databases = completionDatabasesCache.value[connectionId] ?? databaseNamesFromTree(connectionId);
|
||||
const normalized = filter.trim().toLowerCase();
|
||||
return databases
|
||||
.filter((database) => fuzzyTextMatch(database, normalized))
|
||||
.sort((a, b) => tableMatchScore({ name: b }, normalized) - tableMatchScore({ name: a }, normalized))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function lookupLocalCompletionColumns(connectionId: string, database: string, table: string, schema?: string): SqlCompletionColumn[] {
|
||||
return completionColumnIndex.get(completionColumnsKey(connectionId, database, table, schema))?.columns ?? [];
|
||||
}
|
||||
|
||||
function databaseNamesFromTree(connectionId: string): string[] {
|
||||
const node = findNode(treeNodes.value, connectionId);
|
||||
if (!node?.children) return [];
|
||||
const seen = new Set<string>();
|
||||
const names: string[] = [];
|
||||
for (const child of node.children) {
|
||||
if (child.type !== "database" || !child.database) continue;
|
||||
const key = child.database.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
names.push(child.database);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
async function listCompletionDatabases(connectionId: string): Promise<string[]> {
|
||||
if (completionDatabasesCache.value[connectionId]) {
|
||||
return completionDatabasesCache.value[connectionId];
|
||||
}
|
||||
return withCompletionInFlight(`${connectionId}:completion-databases`, async () => {
|
||||
await ensureConnected(connectionId);
|
||||
const config = getConfig(connectionId);
|
||||
const databases = await api.listDatabases(connectionId);
|
||||
completionDatabasesCache.value[connectionId] = filterDatabaseNamesForConnection(
|
||||
databases.map((database) => database.name),
|
||||
config,
|
||||
);
|
||||
return completionDatabasesCache.value[connectionId];
|
||||
});
|
||||
}
|
||||
|
||||
async function listCompletionSchemas(connectionId: string, database: string): Promise<string[]> {
|
||||
const cacheKey = `${connectionId}:${database}`;
|
||||
if (schemaListCache.value[cacheKey]) {
|
||||
@@ -1882,6 +1923,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return listCompletionSchemas(connectionId, database);
|
||||
}
|
||||
|
||||
function refreshCompletionDatabases(connectionId: string): Promise<string[]> {
|
||||
return listCompletionDatabases(connectionId);
|
||||
}
|
||||
|
||||
function refreshCompletionColumns(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionColumn[]> {
|
||||
return listCompletionColumns(connectionId, database, table, schema);
|
||||
}
|
||||
@@ -2378,14 +2423,17 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
listCompletionObjects,
|
||||
listCompletionColumns,
|
||||
listCompletionSchemas,
|
||||
listCompletionDatabases,
|
||||
lookupLocalCompletionTables,
|
||||
lookupLocalCompletionObjects,
|
||||
lookupLocalCompletionColumns,
|
||||
lookupLocalCompletionSchemas,
|
||||
lookupLocalCompletionDatabases,
|
||||
refreshCompletionTables,
|
||||
refreshCompletionObjects,
|
||||
refreshCompletionColumns,
|
||||
refreshCompletionSchemas,
|
||||
refreshCompletionDatabases,
|
||||
listElasticsearchCompletionIndices,
|
||||
exportConnectionsToFile,
|
||||
readImportFile,
|
||||
|
||||
Reference in New Issue
Block a user