feat(editor): improve qualified table completion
This commit is contained in:
parent
3d935f7eb6
commit
42eaddac45
|
|
@ -36,6 +36,7 @@ import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
|||
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
|
||||
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
|
||||
import { resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
import { extractIdentifierAt, isSqlKeyword, matchTable, splitQualifiedIdentifier } from "@/lib/sql/sqlNavigation";
|
||||
import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sql/sqlDiagnostics";
|
||||
import {
|
||||
|
|
@ -1688,10 +1689,6 @@ 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[] = [];
|
||||
|
|
@ -1704,6 +1701,11 @@ function mergeCompletionQualifierNames(primary: string[], secondary: string[]):
|
|||
return merged;
|
||||
}
|
||||
|
||||
function localCompletionDatabaseNames(completionContext: ReturnType<typeof getSqlCompletionContext>): string[] {
|
||||
if (!supportsDatabaseQualifierCompletion() || !completionContext.suggestTables || completionContext.insertTable || !props.connectionId) return [];
|
||||
return connectionStore.lookupLocalCompletionDatabases(props.connectionId, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES);
|
||||
}
|
||||
|
||||
function completionOptionForItem(item: QueryCompletionItem) {
|
||||
const record = () => {
|
||||
recordCompletionSelection(item.label, item.type);
|
||||
|
|
@ -2009,13 +2011,16 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
|||
|
||||
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 databaseNames = localCompletionDatabaseNames(completionContext);
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
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, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) : cachedTables;
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: props.database,
|
||||
currentSchema: props.schema,
|
||||
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema) : cachedTables;
|
||||
|
||||
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
|
||||
const completionObjects = shouldLoadObjects
|
||||
|
|
@ -2104,10 +2109,16 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
|||
const tableNameCompletion = isTableNameCompletionContext(completionContext);
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
const schema = completionContext.qualifier && completionContext.suggestTables ? completionContext.qualifier : props.schema;
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: database,
|
||||
currentSchema: props.schema,
|
||||
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: localCompletionDatabaseNames(completionContext),
|
||||
});
|
||||
if (!localOnlyMetadata && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||
void connectionStore
|
||||
.refreshCompletionTables(connectionId, database, completionContext.qualifier && !schema ? completionContext.qualifier : completionContext.prefix, MAX_COMPLETION_TABLES, schema)
|
||||
.refreshCompletionTables(connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
|
||||
.then((tables) => {
|
||||
cachedTables = mergeCompletionTables(cachedTables, tables);
|
||||
if (completionContext.suggestTables && completionContext.referencedTables.length > 0) {
|
||||
|
|
@ -2220,8 +2231,8 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
}
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
let databaseNames: string[] = [];
|
||||
if (!localOnlyMetadata && supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable) {
|
||||
let databaseNames = localCompletionDatabaseNames(completionContext);
|
||||
if (!localOnlyMetadata && supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable && !completionContext.qualifier) {
|
||||
try {
|
||||
databaseNames = await connectionStore.listCompletionDatabases(props.connectionId!);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
|
@ -2229,14 +2240,17 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
databaseNames = [];
|
||||
}
|
||||
}
|
||||
const qualifierDatabase = completionContext.qualifier ? findExactName(databaseNames, completionContext.qualifier) : undefined;
|
||||
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 && !qualifierDatabase ? completionContext.qualifier : completionContext.prefix;
|
||||
const tableLookupTarget = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: props.database!,
|
||||
currentSchema: props.schema,
|
||||
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
let tables = shouldLoadTables
|
||||
? localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema)
|
||||
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema)
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
|
||||
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
|
||||
: cachedTables;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
|
|
@ -2275,7 +2289,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
|||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (completionContext.qualifier && !qualifierDatabase && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
if (completionContext.qualifier && !tableLookupTarget.qualifierDatabase && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
let schemaTables = connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
if (!localOnlyMetadata) {
|
||||
schemaTables = await listCompletionTablesWithLatencyBudget(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSqlCompletionTableLookupTarget } from "@/lib/sql/sqlCompletionLookupTarget";
|
||||
|
||||
describe("sqlCompletionLookupTarget", () => {
|
||||
it("treats qualified table completion as a database lookup for MySQL-compatible engines", () => {
|
||||
const target = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: "default_db",
|
||||
supportsDatabaseQualifier: true,
|
||||
completionContext: {
|
||||
qualifier: "game_data",
|
||||
prefix: "",
|
||||
suggestTables: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(target).toEqual({
|
||||
database: "game_data",
|
||||
filter: "",
|
||||
qualifierDatabase: "game_data",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the known database casing when the qualifier matches locally", () => {
|
||||
const target = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: "default_db",
|
||||
supportsDatabaseQualifier: true,
|
||||
knownDatabases: ["Game_Data"],
|
||||
completionContext: {
|
||||
qualifier: "game_data",
|
||||
prefix: "ord",
|
||||
suggestTables: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(target).toEqual({
|
||||
database: "Game_Data",
|
||||
filter: "ord",
|
||||
qualifierDatabase: "Game_Data",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps schema-aware qualified table completion scoped to the schema", () => {
|
||||
const target = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: "app",
|
||||
currentSchema: "public",
|
||||
supportsDatabaseQualifier: false,
|
||||
completionContext: {
|
||||
qualifier: "sales",
|
||||
prefix: "ord",
|
||||
suggestTables: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(target).toEqual({
|
||||
database: "app",
|
||||
schema: "sales",
|
||||
filter: "ord",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the current schema for unqualified table completion", () => {
|
||||
const target = resolveSqlCompletionTableLookupTarget({
|
||||
currentDatabase: "app",
|
||||
currentSchema: "public",
|
||||
supportsDatabaseQualifier: true,
|
||||
completionContext: {
|
||||
prefix: "ord",
|
||||
suggestTables: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(target).toEqual({
|
||||
database: "app",
|
||||
schema: "public",
|
||||
filter: "ord",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import type { SqlCompletionContext } from "@/lib/sql/sqlCompletion";
|
||||
|
||||
export interface SqlCompletionTableLookupTarget {
|
||||
database: string;
|
||||
schema?: string;
|
||||
filter: string;
|
||||
qualifierDatabase?: string;
|
||||
}
|
||||
|
||||
function findExactName(names: readonly string[] | undefined, value: string): string | undefined {
|
||||
return names?.find((name) => name.toLowerCase() === value.toLowerCase());
|
||||
}
|
||||
|
||||
export function resolveSqlCompletionTableLookupTarget(options: {
|
||||
currentDatabase: string;
|
||||
currentSchema?: string;
|
||||
supportsDatabaseQualifier: boolean;
|
||||
completionContext: Pick<SqlCompletionContext, "qualifier" | "prefix" | "suggestTables" | "insertTable">;
|
||||
knownDatabases?: readonly string[];
|
||||
}): SqlCompletionTableLookupTarget {
|
||||
const { completionContext } = options;
|
||||
const qualifier = completionContext.qualifier?.trim();
|
||||
const qualifierIsDatabase = options.supportsDatabaseQualifier && !!qualifier && completionContext.suggestTables && !completionContext.insertTable;
|
||||
|
||||
if (qualifierIsDatabase) {
|
||||
// MySQL-compatible engines, including OceanBase MySQL mode, use
|
||||
// database.table. Do not block table completion on a separate database-list
|
||||
// request when the user already typed the database qualifier.
|
||||
const database = findExactName(options.knownDatabases, qualifier) ?? qualifier;
|
||||
return {
|
||||
database,
|
||||
filter: completionContext.prefix,
|
||||
qualifierDatabase: database,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
database: options.currentDatabase,
|
||||
schema: qualifier && completionContext.suggestTables ? qualifier : options.currentSchema,
|
||||
filter: qualifier && completionContext.suggestTables ? completionContext.prefix : qualifier || completionContext.prefix,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue