diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 0f4bed7deb..47af8a5a80 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -176,6 +176,44 @@ const SQL_FUNCTION_NAMES = [ "DATE_SUB", "EXTRACT", "NOW", + "CURRENT_DATE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "CURDATE", + "CURTIME", + "LOCALTIME", + "LOCALTIMESTAMP", + "UTC_DATE", + "UTC_TIME", + "UTC_TIMESTAMP", + "SYSDATE", + "DATE", + "TIME", + "TIMESTAMPDIFF", + "YEAR", + "MONTH", + "DAY", + "HOUR", + "MINUTE", + "SECOND", + "DAYOFWEEK", + "DAYOFYEAR", + "LAST_DAY", + "STR_TO_DATE", + "IF", + "LEFT", + "RIGHT", + "SUBSTRING_INDEX", + "CHAR_LENGTH", + "INSTR", + "LOCATE", + "LPAD", + "RPAD", + "FIND_IN_SET", + "RAND", + "MD5", + "SHA1", + "SHA2", "ROUND", "FLOOR", "CEIL", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 1f3039f91a..4782e319df 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -636,6 +636,44 @@ export default { DATE_SUB: "Subtracts an interval from a date", EXTRACT: "Extracts a date part", NOW: "Returns the current date and time", + CURRENT_DATE: "Returns the current date", + CURRENT_TIME: "Returns the current time", + CURRENT_TIMESTAMP: "Returns the current date and time", + CURDATE: "Returns the current date", + CURTIME: "Returns the current time", + LOCALTIME: "Returns the current local time", + LOCALTIMESTAMP: "Returns the current local date and time", + UTC_DATE: "Returns the current UTC date", + UTC_TIME: "Returns the current UTC time", + UTC_TIMESTAMP: "Returns the current UTC date and time", + SYSDATE: "Returns the current date and time", + DATE: "Extracts the date part", + TIME: "Extracts the time part", + TIMESTAMPDIFF: "Returns the difference between two datetimes", + YEAR: "Extracts the year", + MONTH: "Extracts the month", + DAY: "Extracts the day", + HOUR: "Extracts the hour", + MINUTE: "Extracts the minute", + SECOND: "Extracts the second", + DAYOFWEEK: "Returns the weekday index", + DAYOFYEAR: "Returns the day of year", + LAST_DAY: "Returns the last day of the month", + STR_TO_DATE: "Converts a string to a date", + IF: "Returns a value based on a condition", + LEFT: "Returns the leftmost characters", + RIGHT: "Returns the rightmost characters", + SUBSTRING_INDEX: "Returns a substring before a delimiter count", + CHAR_LENGTH: "Returns the character length", + INSTR: "Returns the position of a substring", + LOCATE: "Returns the position of a substring", + LPAD: "Left-pads a string to a length", + RPAD: "Right-pads a string to a length", + FIND_IN_SET: "Returns the index in a comma-separated list", + RAND: "Returns a random floating-point value", + MD5: "Returns the MD5 hash", + SHA1: "Returns the SHA1 hash", + SHA2: "Returns the SHA2 hash", ROUND: "Rounds to the specified decimal places", FLOOR: "Rounds down", CEIL: "Rounds up", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index ed85c100c0..cfb2976d7e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -617,6 +617,44 @@ export default withEnglishFallback({ DATE_SUB: "Resta un intervalo de una fecha", EXTRACT: "Extrae una parte de la fecha", NOW: "Devuelve la fecha y hora actual", + CURRENT_DATE: "Devuelve la fecha actual", + CURRENT_TIME: "Devuelve la hora actual", + CURRENT_TIMESTAMP: "Devuelve la fecha y hora actual", + CURDATE: "Devuelve la fecha actual", + CURTIME: "Devuelve la hora actual", + LOCALTIME: "Devuelve la hora local actual", + LOCALTIMESTAMP: "Devuelve la fecha y hora local actual", + UTC_DATE: "Devuelve la fecha UTC actual", + UTC_TIME: "Devuelve la hora UTC actual", + UTC_TIMESTAMP: "Devuelve la fecha y hora UTC actual", + SYSDATE: "Devuelve la fecha y hora actual", + DATE: "Extrae la parte de fecha", + TIME: "Extrae la parte de hora", + TIMESTAMPDIFF: "Devuelve la diferencia entre dos fechas y horas", + YEAR: "Extrae el año", + MONTH: "Extrae el mes", + DAY: "Extrae el día", + HOUR: "Extrae la hora", + MINUTE: "Extrae el minuto", + SECOND: "Extrae el segundo", + DAYOFWEEK: "Devuelve el índice del día de la semana", + DAYOFYEAR: "Devuelve el día del año", + LAST_DAY: "Devuelve el último día del mes", + STR_TO_DATE: "Convierte una cadena en fecha", + IF: "Devuelve un valor según una condición", + LEFT: "Devuelve los caracteres más a la izquierda", + RIGHT: "Devuelve los caracteres más a la derecha", + SUBSTRING_INDEX: "Devuelve una subcadena antes de un delimitador", + CHAR_LENGTH: "Devuelve la longitud en caracteres", + INSTR: "Devuelve la posición de una subcadena", + LOCATE: "Devuelve la posición de una subcadena", + LPAD: "Rellena una cadena por la izquierda", + RPAD: "Rellena una cadena por la derecha", + FIND_IN_SET: "Devuelve el índice en una lista separada por comas", + RAND: "Devuelve un valor flotante aleatorio", + MD5: "Devuelve el hash MD5", + SHA1: "Devuelve el hash SHA1", + SHA2: "Devuelve el hash SHA2", ROUND: "Redondea a los decimales especificados", FLOOR: "Redondea hacia abajo", CEIL: "Redondea hacia arriba", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 77ad08ba7e..f06c919d2f 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -615,6 +615,44 @@ export default withEnglishFallback({ DATE_SUB: "Sottrae un intervallo da una data", EXTRACT: "Estrae una parte di data", NOW: "Restituisce la data e l'ora correnti", + CURRENT_DATE: "Restituisce la data corrente", + CURRENT_TIME: "Restituisce l'ora corrente", + CURRENT_TIMESTAMP: "Restituisce la data e l'ora correnti", + CURDATE: "Restituisce la data corrente", + CURTIME: "Restituisce l'ora corrente", + LOCALTIME: "Restituisce l'ora locale corrente", + LOCALTIMESTAMP: "Restituisce la data e l'ora locale correnti", + UTC_DATE: "Restituisce la data UTC corrente", + UTC_TIME: "Restituisce l'ora UTC corrente", + UTC_TIMESTAMP: "Restituisce la data e l'ora UTC correnti", + SYSDATE: "Restituisce la data e l'ora correnti", + DATE: "Estrae la parte data", + TIME: "Estrae la parte ora", + TIMESTAMPDIFF: "Restituisce la differenza tra due date/ore", + YEAR: "Estrae l'anno", + MONTH: "Estrae il mese", + DAY: "Estrae il giorno", + HOUR: "Estrae l'ora", + MINUTE: "Estrae il minuto", + SECOND: "Estrae il secondo", + DAYOFWEEK: "Restituisce l'indice del giorno della settimana", + DAYOFYEAR: "Restituisce il giorno dell'anno", + LAST_DAY: "Restituisce l'ultimo giorno del mese", + STR_TO_DATE: "Converte una stringa in data", + IF: "Restituisce un valore in base a una condizione", + LEFT: "Restituisce i caratteri più a sinistra", + RIGHT: "Restituisce i caratteri più a destra", + SUBSTRING_INDEX: "Restituisce una sottostringa prima di un delimitatore", + CHAR_LENGTH: "Restituisce la lunghezza in caratteri", + INSTR: "Restituisce la posizione di una sottostringa", + LOCATE: "Restituisce la posizione di una sottostringa", + LPAD: "Riempie una stringa a sinistra", + RPAD: "Riempie una stringa a destra", + FIND_IN_SET: "Restituisce l'indice in un elenco separato da virgole", + RAND: "Restituisce un valore mobile casuale", + MD5: "Restituisce l'hash MD5", + SHA1: "Restituisce l'hash SHA1", + SHA2: "Restituisce l'hash SHA2", ROUND: "Arrotonda alla quantità specificata di decimali", FLOOR: "Arrotonda per difetto", CEIL: "Arrotonda per eccesso", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index b4254893e4..8a53f47168 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -614,6 +614,44 @@ export default withEnglishFallback({ DATE_SUB: "日付から間隔を減算", EXTRACT: "日付の一部を抽出", NOW: "現在の日時を返す", + CURRENT_DATE: "現在の日付を返す", + CURRENT_TIME: "現在の時刻を返す", + CURRENT_TIMESTAMP: "現在の日時を返す", + CURDATE: "現在の日付を返す", + CURTIME: "現在の時刻を返す", + LOCALTIME: "現在のローカル時刻を返す", + LOCALTIMESTAMP: "現在のローカル日時を返す", + UTC_DATE: "現在の UTC 日付を返す", + UTC_TIME: "現在の UTC 時刻を返す", + UTC_TIMESTAMP: "現在の UTC 日時を返す", + SYSDATE: "現在の日時を返す", + DATE: "日付部分を抽出", + TIME: "時刻部分を抽出", + TIMESTAMPDIFF: "二つの日時の差を返す", + YEAR: "年を抽出", + MONTH: "月を抽出", + DAY: "日を抽出", + HOUR: "時を抽出", + MINUTE: "分を抽出", + SECOND: "秒を抽出", + DAYOFWEEK: "曜日インデックスを返す", + DAYOFYEAR: "年内通算日を返す", + LAST_DAY: "月末日を返す", + STR_TO_DATE: "文字列を日付に変換", + IF: "条件に応じた値を返す", + LEFT: "左端の文字を返す", + RIGHT: "右端の文字を返す", + SUBSTRING_INDEX: "区切り文字で部分文字列を返す", + CHAR_LENGTH: "文字数を返す", + INSTR: "部分文字列の位置を返す", + LOCATE: "部分文字列の位置を返す", + LPAD: "文字列を左埋めする", + RPAD: "文字列を右埋めする", + FIND_IN_SET: "カンマ区切りリスト内の位置を返す", + RAND: "乱数を返す", + MD5: "MD5 ハッシュを返す", + SHA1: "SHA1 ハッシュを返す", + SHA2: "SHA2 ハッシュを返す", ROUND: "指定された小数点以下桁数で丸める", FLOOR: "切り捨て", CEIL: "切り上げ", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 25bbc01cea..81da731162 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -616,6 +616,44 @@ export default withEnglishFallback({ DATE_SUB: "Subtrai um intervalo de uma data", EXTRACT: "Extrai uma parte da data", NOW: "Retorna a data e hora atuais", + CURRENT_DATE: "Retorna a data atual", + CURRENT_TIME: "Retorna a hora atual", + CURRENT_TIMESTAMP: "Retorna a data e hora atuais", + CURDATE: "Retorna a data atual", + CURTIME: "Retorna a hora atual", + LOCALTIME: "Retorna a hora local atual", + LOCALTIMESTAMP: "Retorna a data e hora local atuais", + UTC_DATE: "Retorna a data UTC atual", + UTC_TIME: "Retorna a hora UTC atual", + UTC_TIMESTAMP: "Retorna a data e hora UTC atuais", + SYSDATE: "Retorna a data e hora atuais", + DATE: "Extrai a parte da data", + TIME: "Extrai a parte da hora", + TIMESTAMPDIFF: "Retorna a diferença entre duas datas/horas", + YEAR: "Extrai o ano", + MONTH: "Extrai o mês", + DAY: "Extrai o dia", + HOUR: "Extrai a hora", + MINUTE: "Extrai o minuto", + SECOND: "Extrai o segundo", + DAYOFWEEK: "Retorna o índice do dia da semana", + DAYOFYEAR: "Retorna o dia do ano", + LAST_DAY: "Retorna o último dia do mês", + STR_TO_DATE: "Converte uma string em data", + IF: "Retorna um valor com base em uma condição", + LEFT: "Retorna os caracteres mais à esquerda", + RIGHT: "Retorna os caracteres mais à direita", + SUBSTRING_INDEX: "Retorna uma substring antes de um delimitador", + CHAR_LENGTH: "Retorna o comprimento em caracteres", + INSTR: "Retorna a posição de uma substring", + LOCATE: "Retorna a posição de uma substring", + LPAD: "Preenche uma string à esquerda", + RPAD: "Preenche uma string à direita", + FIND_IN_SET: "Retorna o índice em uma lista separada por vírgulas", + RAND: "Retorna um valor flutuante aleatório", + MD5: "Retorna o hash MD5", + SHA1: "Retorna o hash SHA1", + SHA2: "Retorna o hash SHA2", ROUND: "Arredonda para o número de casas decimais especificado", FLOOR: "Arredonda para baixo", CEIL: "Arredonda para cima", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 837cc88f55..3f3734f4a6 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -638,6 +638,44 @@ export default withEnglishFallback({ DATE_SUB: "对日期进行减法运算", EXTRACT: "提取日期中的指定部分", NOW: "返回当前日期时间", + CURRENT_DATE: "返回当前日期", + CURRENT_TIME: "返回当前时间", + CURRENT_TIMESTAMP: "返回当前日期时间", + CURDATE: "返回当前日期", + CURTIME: "返回当前时间", + LOCALTIME: "返回当前本地时间", + LOCALTIMESTAMP: "返回当前本地日期时间", + UTC_DATE: "返回当前 UTC 日期", + UTC_TIME: "返回当前 UTC 时间", + UTC_TIMESTAMP: "返回当前 UTC 日期时间", + SYSDATE: "返回当前日期时间", + DATE: "提取日期部分", + TIME: "提取时间部分", + TIMESTAMPDIFF: "计算两个日期时间的差值", + YEAR: "提取年份", + MONTH: "提取月份", + DAY: "提取日", + HOUR: "提取小时", + MINUTE: "提取分钟", + SECOND: "提取秒", + DAYOFWEEK: "返回星期索引", + DAYOFYEAR: "返回一年中的第几天", + LAST_DAY: "返回当月最后一天", + STR_TO_DATE: "将字符串转换为日期", + IF: "按条件返回值", + LEFT: "返回左侧字符", + RIGHT: "返回右侧字符", + SUBSTRING_INDEX: "按分隔符截取子串", + CHAR_LENGTH: "返回字符长度", + INSTR: "返回子串位置", + LOCATE: "返回子串位置", + LPAD: "左侧填充字符串", + RPAD: "右侧填充字符串", + FIND_IN_SET: "返回在逗号分隔列表中的位置", + RAND: "返回随机浮点数", + MD5: "返回 MD5 哈希", + SHA1: "返回 SHA1 哈希", + SHA2: "返回 SHA2 哈希", ROUND: "四舍五入到指定小数位", FLOOR: "向下取整", CEIL: "向上取整", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a6cad74f82..f8875b7be2 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -616,6 +616,44 @@ export default withEnglishFallback({ DATE_SUB: "對日期進行減法運算", EXTRACT: "擷取日期部分", NOW: "回傳目前日期與時間", + CURRENT_DATE: "回傳目前日期", + CURRENT_TIME: "回傳目前時間", + CURRENT_TIMESTAMP: "回傳目前日期與時間", + CURDATE: "回傳目前日期", + CURTIME: "回傳目前時間", + LOCALTIME: "回傳目前本地時間", + LOCALTIMESTAMP: "回傳目前本地日期與時間", + UTC_DATE: "回傳目前 UTC 日期", + UTC_TIME: "回傳目前 UTC 時間", + UTC_TIMESTAMP: "回傳目前 UTC 日期與時間", + SYSDATE: "回傳目前日期與時間", + DATE: "擷取日期部分", + TIME: "擷取時間部分", + TIMESTAMPDIFF: "計算兩個日期時間的差值", + YEAR: "擷取年份", + MONTH: "擷取月份", + DAY: "擷取日", + HOUR: "擷取小時", + MINUTE: "擷取分鐘", + SECOND: "擷取秒", + DAYOFWEEK: "回傳星期索引", + DAYOFYEAR: "回傳一年中的第幾天", + LAST_DAY: "回傳當月最後一天", + STR_TO_DATE: "將字串轉換為日期", + IF: "依條件回傳值", + LEFT: "回傳左側字元", + RIGHT: "回傳右側字元", + SUBSTRING_INDEX: "依分隔符號擷取子字串", + CHAR_LENGTH: "回傳字元長度", + INSTR: "回傳子字串位置", + LOCATE: "回傳子字串位置", + LPAD: "左側填充字串", + RPAD: "右側填充字串", + FIND_IN_SET: "回傳在逗號分隔清單中的位置", + RAND: "回傳隨機浮點數", + MD5: "回傳 MD5 雜湊", + SHA1: "回傳 SHA1 雜湊", + SHA2: "回傳 SHA2 雜湊", ROUND: "四捨五入到指定小數位", FLOOR: "向下捨入", CEIL: "向上捨入", diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 274d8c2f92..f851d60a52 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -724,6 +724,44 @@ function getFunctionDescriptions(t?: SqlCompletionTranslations): Map([ ["DATE_FORMAT", ["date", "format"]], ["DATEDIFF", ["date1", "date2"]], ["TIMESTAMPDIFF", ["unit", "datetime_expr1", "datetime_expr2"]], - ["DATE_ADD", ["date", "interval"]], - ["DATE_SUB", ["date", "interval"]], + ["DATE_ADD", ["date", "INTERVAL expr unit"]], + ["DATE_SUB", ["date", "INTERVAL expr unit"]], ["EXTRACT", ["unit", "date"]], ["YEAR", ["date"]], ["MONTH", ["date"]], @@ -967,6 +1005,48 @@ const MYSQL_FUNCTION_SIGNATURES = new Map([ ["FROM_UNIXTIME", ["unix_timestamp"]], ["UNIX_TIMESTAMP", []], ["SYSDATE", []], + ["CURRENT_DATE", []], + ["CURRENT_TIME", []], + ["CURRENT_TIMESTAMP", []], + ["CURDATE", []], + ["CURTIME", []], + ["LOCALTIME", []], + ["LOCALTIMESTAMP", []], + ["UTC_DATE", []], + ["UTC_TIME", []], + ["UTC_TIMESTAMP", []], + ["DATE", ["expression"]], + ["TIME", ["expression"]], + ["DATE_ADD", ["date", "INTERVAL expr unit"]], + ["DATE_SUB", ["date", "INTERVAL expr unit"]], + ["DATEDIFF", ["date1", "date2"]], + ["TIMESTAMPDIFF", ["unit", "datetime_expr1", "datetime_expr2"]], + ["YEAR", ["date"]], + ["MONTH", ["date"]], + ["DAY", ["date"]], + ["HOUR", ["datetime"]], + ["MINUTE", ["datetime"]], + ["SECOND", ["datetime"]], + ["DAYOFWEEK", ["date"]], + ["DAYOFYEAR", ["date"]], + ["LAST_DAY", ["date"]], + ["STR_TO_DATE", ["string", "format"]], + ["IFNULL", ["expression", "fallback"]], + ["IF", ["condition", "true_value", "false_value"]], + ["CONCAT_WS", ["separator", "...values"]], + ["LEFT", ["string", "length"]], + ["RIGHT", ["string", "length"]], + ["SUBSTRING_INDEX", ["string", "delimiter", "count"]], + ["CHAR_LENGTH", ["string"]], + ["INSTR", ["string", "substring"]], + ["LOCATE", ["substring", "string"]], + ["LPAD", ["string", "length", "pad"]], + ["RPAD", ["string", "length", "pad"]], + ["FIND_IN_SET", ["string", "string_list"]], + ["RAND", []], + ["MD5", ["string"]], + ["SHA1", ["string"]], + ["SHA2", ["string", "bit_length"]], ["JSON_EXTRACT", ["json", "path"]], ["JSON_UNQUOTE", ["json"]], ["GROUP_CONCAT", ["expression"]], @@ -974,6 +1054,9 @@ const MYSQL_FUNCTION_SIGNATURES = new Map([ ["NOW", []], ]); +/** Keywords that may also exist as built-in functions; keep both completion entries. */ +const DUAL_ROLE_SQL_KEYWORDS = new Set(["LEFT", "RIGHT", "IF"]); + const SQLITE_FUNCTION_SIGNATURES = new Map([ ["JSON_EXTRACT", ["json", "path"]], ["JSON_SET", ["json", "path", "value"]], @@ -1052,6 +1135,11 @@ const DATABASE_FUNCTION_SIGNATURES: Partial([ + ["DATE_ADD", "DATE_ADD(${date}, INTERVAL ${expr} ${unit})"], + ["DATE_SUB", "DATE_SUB(${date}, INTERVAL ${expr} ${unit})"], +]); + const COMMON_SQL_FUNCTION_NAMES = new Set([ "COUNT", "SUM", @@ -1265,6 +1353,7 @@ export interface SqlCompletionContext { autoAliasTableCompletions: boolean; tableAliasAfterCursor?: boolean; contextKind: SqlCompletionContextKind; + dataTypeContext: boolean; } export interface SqlFunctionSignatureHelp { @@ -1356,7 +1445,7 @@ class SqlCompletionProvider { this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase)); } if (!preferReferencedColumns || context.suggestRoutines) { - const functionItems = buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType); + const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType); this.items.push(...(preferReferencedColumns ? functionItems.filter((item) => item.label.toLowerCase().startsWith(context.prefix.toLowerCase())) : functionItems)); if (isOracleLikeDatabase(this.databaseType)) { this.items.push(...buildOracleSystemValueItems(context.prefix, this.input.keywordCase)); @@ -1845,6 +1934,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet const suggestRoutines = inCallRoutineContext || oracleTableFunctionContext || inPotentialPackageMemberContext || (!exclusiveTableSuggestions && !exclusiveColumnSuggestions && !insertInfo && !updateInfo?.inSetClause && prefix.length >= 2); const statementKind = detectStatementKind(beforeCursor || fullStatement); + const dataTypeContext = isCreateTableColumnTypeContext(beforeToken); const preferredKeywords = qualifier ? [] : preferredKeywordsForCompletion(beforeCursor, beforeToken, selectListColumnContext, exclusiveTableSuggestions, updateInfo, deleteInfo); const contextKind = detectCompletionContextKind({ qualifier, @@ -1892,9 +1982,30 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet autoAliasTableCompletions, tableAliasAfterCursor, contextKind, + dataTypeContext, }; } +function isCreateTableColumnTypeContext(beforeToken: string): boolean { + const cleaned = stripSqlLiterals(beforeToken); + if (!/^\s*create\s+table\b/i.test(cleaned)) return false; + + const bodyStart = cleaned.indexOf("("); + if (bodyStart < 0) return false; + + let depth = 0; + let segmentStart = bodyStart + 1; + for (let index = bodyStart + 1; index < cleaned.length; index += 1) { + const char = cleaned[index]; + if (char === "(") depth += 1; + else if (char === ")") depth = Math.max(0, depth - 1); + else if (char === "," && depth === 0) segmentStart = index + 1; + } + + const segment = cleaned.slice(segmentStart).trim(); + return /^(?:[A-Za-z_][\w$]*|`[^`]+`|"[^"]+"|\[[^\]]+\])$/.test(segment); +} + function removeActiveTableCompletionReference(referencedTables: SqlCompletionReferencedTable[], prefix: string, qualifier?: string): SqlCompletionReferencedTable[] { const activeName = normalizeIdentifierPart(prefix); const activeQualifier = qualifier ? normalizeIdentifierPart(qualifier) : undefined; @@ -4017,7 +4128,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map { - if (functionSignatures.has(keyword)) return false; + if (functionSignatures.has(keyword) && !DATA_TYPE_KEYWORDS.has(keyword) && !DUAL_ROLE_SQL_KEYWORDS.has(keyword)) return false; if (WINDOW_FUNCTIONS.has(keyword)) return false; if (!matchesPrefix(keyword, prefix)) return false; if (!showDdl && isDml && (DDL_ONLY_KEYWORDS.has(keyword) || DATA_TYPE_KEYWORDS.has(keyword))) return false; diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 74ce32d596..b1a918c94d 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -133,6 +133,46 @@ test("suggests database-specific data types and functions", () => { columnsByTable: new Map(), databaseType: "mysql", }); + const mysqlCurrentDateItems = buildSqlCompletionItems("select current_d", "select current_d".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlCurrentTimestampItems = buildSqlCompletionItems("select current_t", "select current_t".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlCurdateItems = buildSqlCompletionItems("select curd", "select curd".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlIfnullItems = buildSqlCompletionItems("select ifn", "select ifn".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlDateAddItems = buildSqlCompletionItems("select date_a", "select date_a".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlDateSubItems = buildSqlCompletionItems("select date_s", "select date_s".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlSubstringIndexItems = buildSqlCompletionItems("select substring_i", "select substring_i".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlLeftItems = buildSqlCompletionItems("select lef", "select lef".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); const postgresDateItems = buildSqlCompletionItems("select date_f", "select date_f".length, { tables: [], columnsByTable: new Map(), @@ -144,10 +184,48 @@ test("suggests database-specific data types and functions", () => { assert.ok(functionItems.some((item) => item.type === "function" && item.label === "JSONB_BUILD_OBJECT")); assert.ok(mysqlFunctionItems.some((item) => item.type === "function" && item.label === "DATE_FORMAT")); assert.ok(mysqlSysdateItems.some((item) => item.type === "function" && item.label === "SYSDATE")); + assert.ok(mysqlCurrentDateItems.some((item) => item.type === "function" && item.label === "CURRENT_DATE")); + assert.ok(mysqlCurrentTimestampItems.some((item) => item.type === "function" && item.label === "CURRENT_TIMESTAMP")); + assert.ok(mysqlCurrentTimestampItems.some((item) => item.type === "function" && item.label === "CURRENT_TIME")); + assert.ok(mysqlCurdateItems.some((item) => item.type === "function" && item.label === "CURDATE")); + assert.ok(mysqlIfnullItems.some((item) => item.type === "function" && item.label === "IFNULL")); + assert.equal( + mysqlDateAddItems.find((item) => item.type === "function" && item.label === "DATE_ADD")?.apply, + "DATE_ADD(${date}, INTERVAL ${expr} ${unit})", + ); + assert.equal( + mysqlDateSubItems.find((item) => item.type === "function" && item.label === "DATE_SUB")?.apply, + "DATE_SUB(${date}, INTERVAL ${expr} ${unit})", + ); + assert.ok(mysqlSubstringIndexItems.some((item) => item.type === "function" && item.label === "SUBSTRING_INDEX")); + assert.ok(mysqlLeftItems.some((item) => item.type === "function" && item.label === "LEFT")); + assert.ok(mysqlLeftItems.some((item) => item.type === "keyword" && item.label === "LEFT")); assert.equal( postgresDateItems.some((item) => item.type === "function" && item.label === "DATE_FORMAT"), false, ); + + const mysqlDateTypeItems = buildSqlCompletionItems("CREATE TABLE t (d dat", "CREATE TABLE t (d dat".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + const mysqlTimeTypeItems = buildSqlCompletionItems("CREATE TABLE t (tm tim", "CREATE TABLE t (tm tim".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + assert.equal(mysqlDateTypeItems[0]?.label, "DATE"); + assert.equal(mysqlDateTypeItems.some((item) => item.type === "function" && item.label === "DATE"), false); + assert.equal(mysqlTimeTypeItems[0]?.label, "TIME"); + assert.equal(mysqlTimeTypeItems.some((item) => item.type === "function" && item.label === "TIME"), false); + + const mysqlCreateViewItems = buildSqlCompletionItems("CREATE VIEW v AS SELECT dat", "CREATE VIEW v AS SELECT dat".length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + assert.ok(mysqlCreateViewItems.some((item) => item.type === "function" && item.label === "DATE")); }); test("suggests Oracle SQL, PL/SQL, and data type keywords", () => { @@ -772,6 +850,25 @@ test("suggests compound JOIN keywords while typing a join modifier", () => { assert.equal(items[leftJoinIndex]?.apply, "LEFT JOIN "); }); +test("keeps MySQL LEFT JOIN ranking when LEFT() is also a function", () => { + const sql = "select * from users le"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables, + columnsByTable, + databaseType: "mysql", + }); + + const leftJoinIndex = items.findIndex((item) => item.type === "keyword" && item.label === "LEFT JOIN"); + const leftKeywordIndex = items.findIndex((item) => item.type === "keyword" && item.label === "LEFT"); + const leftFunctionIndex = items.findIndex((item) => item.type === "function" && item.label === "LEFT"); + + assert.ok(leftJoinIndex >= 0); + assert.ok(leftKeywordIndex >= 0); + assert.ok(leftFunctionIndex >= 0); + assert.ok(leftJoinIndex < leftKeywordIndex, "LEFT JOIN should rank ahead of LEFT keyword"); + assert.ok(leftJoinIndex < leftFunctionIndex, "LEFT JOIN should rank ahead of LEFT()"); +}); + test("suggests JOIN after a join modifier", () => { const sql = "select * from users left "; const items = buildSqlCompletionItems(sql, sql.length, {