From e7191b1c4b9310124d95ac40e62f2eddf9902492 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 27 Aug 2026 08:40:12 +0800 Subject: [PATCH] fix(completion): add datepart args for sqlserver date functions --- .../sql/sqlCompletion.context.spec.ts | 34 +++++++++++++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 19 ++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts index ec81659aad..379da9ef7c 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -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`; diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 604117ed06..895f847dae 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -1067,6 +1067,9 @@ const SQLSERVER_FUNCTION_SIGNATURES = new Map([ ["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([ ["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;