feat(completion): improve SQL function suggestions

This commit is contained in:
zipg authored and GitHub committed 2026-07-20 01:55:52 +08:00
1 parent 21652bc797
commit f5cdbb8662
12 files changed
+412 -40

No files matched your search

@@ -183,7 +183,7 @@ export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base:
const mutationSchema = mutationTarget?.qualifierParts[mutationTarget.qualifierParts.length - 1];
const suggestTables = scope.kind === "table" || scope.kind === "schema" || scope.kind === "catalog";
const suggestColumns = scope.kind === "columns";
const suggestRoutines = scope.kind === "routine";
const suggestRoutines = scope.kind === "routine" || (suggestColumns && base.suggestRoutines && !base.exclusiveColumnSuggestions);
const projectionAliases = sqlSemanticProjectionAliasColumns(model).map((column) => column.name);
return {
@@ -198,7 +198,7 @@ export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base:
suggestJoinConditions: model.cursorIntent.kind === "join_condition",
exclusiveTableSuggestions: suggestTables,
exclusiveColumnSuggestions: model.cursorIntent.kind === "alias_column" || model.cursorIntent.kind === "insert_column" || model.cursorIntent.kind === "update_column",
exclusiveRoutineSuggestions: suggestRoutines,
exclusiveRoutineSuggestions: scope.kind === "routine",
prioritizeSelectAliases: base.prioritizeSelectAliases || projectionAliases.length > 0,
selectAliases: projectionAliases.length > 0 ? projectionAliases : base.selectAliases,
referencedTables: referencedTables.length > 0 ? referencedTables : base.referencedTables,
+29 -11
View File
@@ -1171,6 +1171,9 @@ export interface SqlCompletionObject {
type: "procedure" | "function" | "trigger" | "package";
parentSchema?: string;
parentName?: string;
dataType?: string;
signature?: string;
comment?: string | null;
applyName?: string;
boost?: number;
}
@@ -1338,10 +1341,15 @@ class SqlCompletionProvider {
}
const preferReferencedColumns = hasMatchingReferencedColumnPrefix(context, this.input.columnsByTable);
if (!pendingJoinKeyword && !preferReferencedColumns && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) {
if (!pendingJoinKeyword && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) {
const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS);
this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase));
this.items.push(...buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType));
if (!preferReferencedColumns) {
this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase));
}
if (!preferReferencedColumns || context.suggestRoutines) {
const functionItems = buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType);
this.items.push(...(preferReferencedColumns ? functionItems.filter((item) => item.label.toLowerCase().startsWith(context.prefix.toLowerCase())) : functionItems));
}
}
if (this.databaseType === "manticoresearch" && context.exclusiveRoutineSuggestions) {
@@ -1818,10 +1826,9 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
const inJoinConditionContext = isInJoinConditionContext(beforeCursor);
const prioritizeSelectAliases = isInOrderOrGroupByContext(beforeCursor);
const inCallRoutineContext = isCallRoutineContext(beforeCursor);
const inPotentialPackageMemberContext = !!qualifier && !exclusiveTableSuggestions && !insertInfo && !oracleTableFunctionContext;
const inPotentialPackageMemberContext = !!qualifier && !exclusiveTableSuggestions && !insertInfo && !updateInfo?.inSetClause && !oracleTableFunctionContext;
const suggestColumns = !!qualifier || !!updateInfo?.inSetClause || !!insertInfo || (inColumnContext && referencedTables.length > 0);
const preferColumnsOverGlobalRoutines = suggestColumns && referencedTables.length > 0 && !qualifier;
const suggestRoutines = inCallRoutineContext || oracleTableFunctionContext || inPotentialPackageMemberContext || (!preferColumnsOverGlobalRoutines && !exclusiveTableSuggestions && !exclusiveColumnSuggestions && !insertInfo && prefix.length >= 2);
const suggestRoutines = inCallRoutineContext || oracleTableFunctionContext || inPotentialPackageMemberContext || (!exclusiveTableSuggestions && !exclusiveColumnSuggestions && !insertInfo && !updateInfo?.inSetClause && prefix.length >= 2);
const statementKind = detectStatementKind(beforeCursor || fullStatement);
const preferredKeywords = qualifier ? [] : preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo);
@@ -1903,12 +1910,12 @@ function detectCompletionContextKind(options: {
if (options.updateInfo?.inSetClause) return "column";
if (options.inCallRoutineContext) return "exec";
if (options.qualifier && options.exclusiveColumnSuggestions) return "alias_column";
if (options.suggestColumns) return options.qualifier ? "alias_column" : "column";
if (options.oracleTableFunctionContext || options.suggestRoutines) return "routine";
if (options.exclusiveTableSuggestions || options.afterTableTrigger) {
if (options.statementKind === "insert" && options.lastWord === "into") return "insert_target";
return options.lastWord === "join" ? "join" : "table";
}
if (options.suggestColumns) return options.qualifier ? "alias_column" : "column";
return "keyword";
}
@@ -2190,7 +2197,7 @@ function detectInsertColumnListContext(beforeCursor: string): { table: string; s
function detectUpdateCompletionContext(beforeCursor: string): { target: { table: string; schema?: string }; afterTarget: boolean; inSetClause: boolean; afterSetAssignments: boolean } | null {
const cleaned = beforeCursor.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
const match = /^\s*update\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)(?:\s+(?:as\s+)?([A-Za-z_][\w$]*))?/i.exec(cleaned);
const match = /^\s*update\s+((?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*)(?:\.(?:"[^"]+"|`[^`]+`|[A-Za-z_][\w$]*))?)(?:\s+(?:as\s+)?((?!set\b|where\b)[A-Za-z_][\w$]*))?/i.exec(cleaned);
if (!match) return null;
const [first, second] = splitQualifiedName(match[1] ?? "");
if (!first) return null;
@@ -2975,9 +2982,10 @@ function buildSchemaItems(prefix: string, schemas: string[], dialect?: "mysql" |
function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionObject[], dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType, currentSchema?: string): SqlCompletionItem[] {
if (completionQualifierIsReferencedTable(context)) return [];
const onlyProcedures = context.contextKind === "exec";
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
const prioritizeOracleFunctions = databaseType === "oracle" && context.statementKind === "select";
return objects
.filter((object) => (!onlyProcedures || object.type === "procedure") && objectMatchesCompletionContext(object, context))
.filter((object) => (!onlyProcedures || object.type === "procedure") && (!onlyFunctions || (object.type === "function" && object.name.toLowerCase().startsWith(context.prefix.toLowerCase()))) && objectMatchesCompletionContext(object, context))
.map((object) => {
const qualifiedByContext = objectIsQualifiedByContext(object, context);
const objectInCurrentSchema = !!currentSchema && !!object.schema && normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(currentSchema);
@@ -2985,13 +2993,17 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
qualifiedByContext || (context.qualifier && object.schema?.toLowerCase() === context.qualifier.toLowerCase())
? quoteSqlIdentifier(object.name, dialect)
: (object.applyName ?? (object.schema && !objectInCurrentSchema ? `${quoteSqlIdentifier(object.schema, dialect)}.${quoteSqlIdentifier(object.name, dialect)}` : quoteSqlIdentifier(object.name, dialect)));
const detail = object.type === "trigger" && object.parentName ? `trigger on ${object.parentName}` : object.parentName ? `${object.type} in ${object.parentName}` : object.schema ? `${object.type} in ${object.schema}` : object.type;
const locationDetail = object.type === "trigger" && object.parentName ? `trigger on ${object.parentName}` : object.parentName ? `${object.type} in ${object.parentName}` : object.schema ? `${object.type} in ${object.schema}` : object.type;
const detail = object.dataType ? `${locationDetail} [${object.dataType}]` : locationDetail;
const schemaBoost = onlyFunctions ? Math.min(object.boost ?? 0, 1000) : (object.boost ?? 0);
const typeBoost = routineTypeBoost(object.type, prioritizeOracleFunctions && !onlyFunctions);
return {
label: object.name,
type: "function" as const,
detail,
info: buildRoutineInfo(object),
apply: object.type === "trigger" || object.type === "package" ? applyName : `${applyName}()`,
boost: computeBoost(object.name, context.prefix) + routineTypeBoost(object.type, prioritizeOracleFunctions) + (object.boost ?? 0),
boost: computeBoost(object.name, context.prefix) + typeBoost + schemaBoost,
dedupeKey: object.applyName || (databaseType === "oracle" && object.schema) ? applyName : undefined,
// Preserve exact routine matches before the capped candidate list is truncated.
exactMatch: !!context.prefix && object.name.toLowerCase() === context.prefix.toLowerCase(),
@@ -3001,6 +3013,12 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
.slice(0, MAX_TABLE_COMPLETION_ITEMS);
}
function buildRoutineInfo(object: SqlCompletionObject): string | undefined {
const qualifiedName = object.parentName ? [object.parentSchema ?? object.schema, object.parentName, object.name].filter(Boolean).join(".") : [object.schema, object.name].filter(Boolean).join(".");
const parts = [qualifiedName || object.name, object.signature?.trim(), object.comment?.trim()].filter((part): part is string => !!part);
return parts.length > 1 ? parts.join("\n") : undefined;
}
function routineTypeBoost(type: SqlCompletionObject["type"], prioritizeFunctions: boolean): number {
if (type === "package") return 1600;
if (type === "function") return prioritizeFunctions ? 1800 : 900;
@@ -7,6 +7,11 @@ export interface SqlCompletionTableLookupTarget {
qualifierDatabase?: string;
}
export interface SqlCompletionRoutineLookupTarget {
schema?: string;
mask: string;
}
function findExactName(names: readonly string[] | undefined, value: string): string | undefined {
return names?.find((name) => name.toLowerCase() === value.toLowerCase());
}
@@ -40,3 +45,15 @@ export function resolveSqlCompletionTableLookupTarget(options: {
filter: qualifier && completionContext.suggestTables ? completionContext.prefix : qualifier || completionContext.prefix,
};
}
export function resolveSqlCompletionRoutineLookupTarget(options: { currentSchema?: string; completionContext: Pick<SqlCompletionContext, "qualifier" | "qualifierParts" | "prefix"> }): SqlCompletionRoutineLookupTarget {
const qualifierParts = options.completionContext.qualifierParts?.filter(Boolean);
const schema = qualifierParts?.[qualifierParts.length - 1] ?? options.completionContext.qualifier?.trim() ?? options.currentSchema;
// A qualified routine uses the qualifier as metadata scope; only the final
// identifier fragment is the function/procedure name mask.
return {
schema: schema || undefined,
mask: options.completionContext.prefix,
};
}