fix(completion): add datepart args for sqlserver date functions

This commit is contained in:
zipg authored and GitHub committed 2026-08-27 08:40:12 +08:00
1 parent 2138f59da4
commit e7191b1c4b
2 files changed
+52 -1

No files matched your search

@@ -26,6 +26,40 @@ describe("sqlCompletion keyword snippets", () => {
});
});
describe("SQL Server datepart completion", () => {
it.each(["DATEADD", "DATEDIFF", "DATEPART", "DATENAME"])("suggests datepart values for the first %s argument", (functionName) => {
const sql = `SELECT ${functionName}(d`;
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [],
columnsByTable: new Map(),
databaseType: "sqlserver",
dialect: "sqlserver",
keywordCase: "lower",
});
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "day", type: "keyword" }), expect.objectContaining({ label: "dayofyear", type: "keyword" })]));
expect(items.some((item) => item.label === "deadlock_priority")).toBe(false);
});
it("auto-opens the datepart list immediately after the opening parenthesis", () => {
const sql = "SELECT DATEADD(";
expect(shouldAutoOpenSqlCompletion(sql, sql.length, { databaseType: "sqlserver", dialect: "sqlserver" })).toBe(true);
});
it("stops offering datepart values after the first argument", () => {
const sql = "SELECT DATEADD(day, d";
expect(getSqlCompletionContext(sql, sql.length, { databaseType: "sqlserver", dialect: "sqlserver" }).preferredValueKeywords).toBeUndefined();
});
it("does not apply SQL Server datepart values to other dialects", () => {
const sql = "SELECT DATEADD(d";
expect(getSqlCompletionContext(sql, sql.length, { databaseType: "mysql", dialect: "mysql" }).preferredValueKeywords).toBeUndefined();
});
});
describe("MySQL DESCRIBE table completion", () => {
it.each(["DESC", "DESCRIBE"])("treats %s as a table-name context", (keyword) => {
const sql = `${keyword} ord`;
+18 -1
View File
@@ -1067,6 +1067,9 @@ const SQLSERVER_FUNCTION_SIGNATURES = new Map<string, string[]>([
["ISNULL", ["expression", "replacement"]],
]);
const SQLSERVER_DATEPART_FUNCTIONS = new Set(["DATEADD", "DATEDIFF", "DATEPART", "DATENAME"]);
const SQLSERVER_DATEPART_VALUES = ["year", "quarter", "month", "dayofyear", "day", "week", "weekday", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond", "tzoffset", "iso_week"];
const MANTICORESEARCH_FUNCTION_SIGNATURES = new Map<string, string[]>([
["MATCH", ["query"]],
["BM25F", ["field=weight", "...fields"]],
@@ -1335,6 +1338,7 @@ export type SqlCompletionContextKind = "table" | "schema" | "catalog" | "routine
export interface SqlCompletionContext {
prefix: string;
replacementRange?: { start: number; end: number };
preferredValueKeywords?: string[];
qualifier?: string;
qualifierParts?: string[];
suggestTables: boolean;
@@ -1515,6 +1519,10 @@ class SqlCompletionProvider {
return dedupeAndSort(buildMongoCompletionItemsFromContext({ mode: "root", prefix: context.prefix, from: 0 }).map(mongoCompletionItemToSqlCompletionItem));
}
if (context.preferredValueKeywords?.length) {
return dedupeAndSort(buildPreferredKeywordItems(context.prefix, context.preferredValueKeywords, this.input.keywordCase));
}
const preferReferencedColumns = hasMatchingReferencedColumnPrefix(context, this.input.columnsByTable);
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);
@@ -1638,7 +1646,7 @@ export function shouldAutoOpenSqlCompletion(sql: string, cursor: number, options
if (isAfterJoinModifierContext(sql.slice(0, cursor))) return true;
if (/\bcall\s+(?:[A-Za-z_][\w$]*\.)?$/i.test(sql.slice(0, cursor))) return true;
const context = getSqlCompletionContext(sql, cursor, options);
if (previousChar === "(" && context.insertTable) return true;
if (previousChar === "(" && (context.insertTable || context.preferredValueKeywords?.length)) return true;
if (/[,;()[\]]/.test(previousChar)) return false;
if (context.exclusiveTableSuggestions || context.exclusiveRoutineSuggestions || context.suggestTables) {
return true;
@@ -2266,6 +2274,7 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
const statementKind = detectStatementKind(beforeCursor || fullStatement);
const dataTypeContext = isCreateTableColumnTypeContext(beforeToken);
const preferredValueKeywords = sqlServerDatepartCompletionValues(beforeCursor, options.databaseType);
const preferredKeywords = qualifier ? [] : preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo);
const contextKind = detectCompletionContextKind({
qualifier,
@@ -2284,6 +2293,7 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
return {
prefix,
preferredValueKeywords,
qualifier: insertInfo ? undefined : qualifier,
qualifierParts: insertInfo ? undefined : qualifierParts,
suggestTables: insertInfo ? false : afterTableTrigger,
@@ -2320,6 +2330,13 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
};
}
function sqlServerDatepartCompletionValues(beforeCursor: string, databaseType?: DatabaseType): string[] | undefined {
if (databaseType !== "sqlserver") return undefined;
const call = findActiveFunctionCall(beforeCursor);
if (!call || call.activeGroup !== 0 || !SQLSERVER_DATEPART_FUNCTIONS.has(call.name.toUpperCase())) return undefined;
return countTopLevelCommas(call.groupText) === 0 ? SQLSERVER_DATEPART_VALUES : undefined;
}
function isCreateTableColumnTypeContext(beforeToken: string): boolean {
const cleaned = stripSqlLiterals(beforeToken);
if (!/^\s*create\s+table\b/i.test(cleaned)) return false;