fix(editor): 内置 SQL 补全模板按数据库方言适配

This commit is contained in:
zhangyc authored and GitHub committed 2026-07-24 23:51:35 +08:00
1 parent 355ca7d3ad
commit 3939658ebe
7 files changed
+353 -133

No files matched your search

+14 -122
View File
@@ -3,6 +3,9 @@ import type { DatabaseType, SqlSnippet } from "@/types/database";
import { buildMongoCompletionItemsFromContext, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
import { CLOUDFLARE_D1_COMMON_FUNCTION_NAMES } from "@/lib/sql/cloudflareD1";
import type { SqlObjectNavigationType } from "@/lib/sql/sqlNavigation";
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
const SQL_KEYWORDS = [
"SELECT",
@@ -778,119 +781,6 @@ function getFunctionDescriptions(t?: SqlCompletionTranslations): Map<string, str
]);
}
export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = [
{
id: "builtin-sel",
label: "select *",
prefix: "sel",
body: "SELECT *\nFROM table\nLIMIT 100;",
enabled: true,
},
{
id: "builtin-ins",
label: "insert into",
prefix: "ins",
body: "INSERT INTO table (columns)\nVALUES (values);",
enabled: true,
},
{
id: "builtin-upd",
label: "update set",
prefix: "upd",
body: "UPDATE table\nSET column = value\nWHERE condition;",
enabled: true,
},
{
id: "builtin-cte",
label: "common table expression",
prefix: "cte",
body: "WITH name AS (\n SELECT columns\n FROM table\n)\nSELECT *\nFROM name;",
enabled: true,
},
{
id: "builtin-join",
label: "join",
prefix: "join",
body: "JOIN table ON left_column = right_column",
enabled: true,
},
{
id: "builtin-case",
label: "case when",
prefix: "case",
body: "CASE\n WHEN condition THEN value\n ELSE default\nEND",
enabled: true,
},
{
id: "builtin-ct",
label: "create table",
prefix: "ct",
body: "CREATE TABLE table (\n column type\n);",
enabled: true,
},
{
id: "builtin-ex",
label: "exists",
prefix: "ex",
body: "EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-nex",
label: "not exists",
prefix: "nex",
body: "NOT EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-at",
label: "alter table add column",
prefix: "at",
body: "ALTER TABLE table\nADD COLUMN column type;",
enabled: true,
},
{
id: "builtin-ci",
label: "create index",
prefix: "ci",
body: "CREATE INDEX idx_name\nON table (column);",
enabled: true,
},
];
const MANTICORESEARCH_SQL_SNIPPETS: SqlSnippet[] = [
{
id: "builtin-manticore-match",
label: "match query",
prefix: "match",
body: "MATCH('query')",
},
{
id: "builtin-manticore-facet",
label: "facet",
prefix: "facet",
body: "FACET column",
},
{
id: "builtin-manticore-show-meta",
label: "show meta",
prefix: "m",
body: "SHOW META;",
},
{
id: "builtin-manticore-show-tables",
label: "show tables",
prefix: "tab",
body: "SHOW TABLES;",
},
{
id: "builtin-manticore-call-pq",
label: "call pq",
prefix: "p",
body: "CALL PQ ('pq', ('{\"title\":\"query\"}'));",
},
];
const SQL_FUNCTION_SIGNATURES = new Map<string, string[]>([
// Aggregate
["COUNT", ["expression"]],
@@ -1446,7 +1336,7 @@ class SqlCompletionProvider {
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);
if (!preferReferencedColumns) {
this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase));
this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase, this.databaseType));
}
if (!preferReferencedColumns || context.suggestRoutines) {
const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType);
@@ -1463,6 +1353,7 @@ class SqlCompletionProvider {
context.prefix,
MANTICORESEARCH_SQL_SNIPPETS.filter((snippet) => snippet.id === "builtin-manticore-call-pq"),
this.input.keywordCase,
this.databaseType,
),
);
}
@@ -3250,9 +3141,9 @@ function applyBuiltinSnippetKeywordCase(snippet: SqlSnippet, text: string, keywo
const BUILTIN_SNIPPET_PLACEHOLDER_RE = /\b(idx_name|left_column|right_column|columns|values|condition|column|default|value|name|type|table)\b/g;
function applyBuiltinSnippetPlaceholders(snippet: SqlSnippet): string {
if (!shouldFormatBuiltinSnippet(snippet)) return snippet.body;
return snippet.body.replace(BUILTIN_SNIPPET_PLACEHOLDER_RE, (match) => `\${${match}}`);
function applyBuiltinSnippetPlaceholders(snippet: SqlSnippet, body = snippet.body): string {
if (!shouldFormatBuiltinSnippet(snippet)) return body;
return body.replace(BUILTIN_SNIPPET_PLACEHOLDER_RE, (match) => `\${${match}}`);
}
function buildPreferredKeywordItems(prefix: string, keywords: string[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
@@ -4108,11 +3999,11 @@ function singularTableName(name: string): string {
return lower;
}
export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
return buildSnippetItems(prefix, snippets, keywordCase);
export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase, databaseType?: DatabaseType): SqlCompletionItem[] {
return buildSnippetItems(prefix, snippets, keywordCase, databaseType);
}
function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase, databaseType?: DatabaseType): SqlCompletionItem[] {
if (!prefix) return [];
return snippets
.filter((snippet) => {
@@ -4131,8 +4022,9 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
const baseBoost = matchesByPrefix ? 4000 : 0;
// Placeholder replacement runs on the original (UPPER-case) body first,
// then keyword casing is applied to both variants uniformly.
const body = applyBuiltinSnippetKeywordCase(snippet, snippet.body, keywordCase);
const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet), keywordCase);
const resolvedBody = resolveSqlSnippetBodyForDatabase(snippet, databaseType);
const body = applyBuiltinSnippetKeywordCase(snippet, resolvedBody, keywordCase);
const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet, resolvedBody), keywordCase);
return {
label: snippet.label,
type: "snippet" as const,
@@ -0,0 +1,215 @@
import type { DatabaseType, SqlSnippet } from "@/types/database";
const DEFAULT_SELECT_ROW_LIMIT = 100;
/**
* Built-in SQL snippets shared by database types.
*
* Database-specific bodies are resolved at completion time so the stored
* defaults remain portable and existing user-customized bodies stay intact.
*/
export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = [
{
id: "builtin-sel",
label: "select *",
prefix: "sel",
body: `SELECT *\nFROM table\nLIMIT ${DEFAULT_SELECT_ROW_LIMIT};`,
enabled: true,
},
{
id: "builtin-ins",
label: "insert into",
prefix: "ins",
body: "INSERT INTO table (columns)\nVALUES (values);",
enabled: true,
},
{
id: "builtin-upd",
label: "update set",
prefix: "upd",
body: "UPDATE table\nSET column = value\nWHERE condition;",
enabled: true,
},
{
id: "builtin-cte",
label: "common table expression",
prefix: "cte",
body: "WITH name AS (\n SELECT columns\n FROM table\n)\nSELECT *\nFROM name;",
enabled: true,
},
{
id: "builtin-join",
label: "join",
prefix: "join",
body: "JOIN table ON left_column = right_column",
enabled: true,
},
{
id: "builtin-case",
label: "case when",
prefix: "case",
body: "CASE\n WHEN condition THEN value\n ELSE default\nEND",
enabled: true,
},
{
id: "builtin-ct",
label: "create table",
prefix: "ct",
body: "CREATE TABLE table (\n column type\n);",
enabled: true,
},
{
id: "builtin-ex",
label: "exists",
prefix: "ex",
body: "EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-nex",
label: "not exists",
prefix: "nex",
body: "NOT EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-at",
label: "alter table add column",
prefix: "at",
body: "ALTER TABLE table\nADD COLUMN column type;",
enabled: true,
},
{
id: "builtin-ci",
label: "create index",
prefix: "ci",
body: "CREATE INDEX idx_name\nON table (column);",
enabled: true,
},
];
/** Additional snippets only exposed by the Manticore Search completion path. */
export const MANTICORESEARCH_SQL_SNIPPETS: SqlSnippet[] = [
{
id: "builtin-manticore-match",
label: "match query",
prefix: "match",
body: "MATCH('query')",
},
{
id: "builtin-manticore-facet",
label: "facet",
prefix: "facet",
body: "FACET column",
},
{
id: "builtin-manticore-show-meta",
label: "show meta",
prefix: "m",
body: "SHOW META;",
},
{
id: "builtin-manticore-show-tables",
label: "show tables",
prefix: "tab",
body: "SHOW TABLES;",
},
{
id: "builtin-manticore-call-pq",
label: "call pq",
prefix: "p",
body: "CALL PQ ('pq', ('{\"title\":\"query\"}'));",
},
];
type BuiltinSqlSnippetBodyBuilder = (databaseType?: DatabaseType) => string;
interface BuiltinSqlSnippetRule {
defaultBody: string;
buildBody: BuiltinSqlSnippetBodyBuilder;
}
type SelectSnippetLimitStyle = "limit" | "top" | "first" | "fetch-first" | "rows" | "rownum" | "unbounded";
const SELECT_SNIPPET_LIMIT_STYLE_BY_DATABASE: Partial<Record<DatabaseType, SelectSnippetLimitStyle>> = {
oracle: "rownum",
"oceanbase-oracle": "rownum",
oscar: "rownum",
dameng: "fetch-first",
db2: "fetch-first",
sqlserver: "top",
access: "top",
iris: "top",
teradata: "top",
informix: "first",
firebird: "rows",
// An unknown JDBC driver may expose any SQL dialect. Known JDBC profiles are
// converted to their effective database type before reaching the editor.
jdbc: "unbounded",
};
const PARENTHESIZED_ADD_COLUMN_DATABASES = new Set<DatabaseType>(["oracle", "oceanbase-oracle", "yashandb", "xugu", "dameng", "iris", "informix"]);
const ADD_COLUMN_WITHOUT_COLUMN_KEYWORD_DATABASES = new Set<DatabaseType>(["sqlserver", "kingbase", "cassandra", "teradata"]);
function buildSelectSnippetBody(databaseType?: DatabaseType): string {
const style = databaseType ? (SELECT_SNIPPET_LIMIT_STYLE_BY_DATABASE[databaseType] ?? "limit") : "limit";
switch (style) {
case "top":
return `SELECT TOP ${DEFAULT_SELECT_ROW_LIMIT} *\nFROM table;`;
case "first":
return `SELECT FIRST ${DEFAULT_SELECT_ROW_LIMIT} *\nFROM table;`;
case "fetch-first":
return `SELECT *\nFROM table\nFETCH FIRST ${DEFAULT_SELECT_ROW_LIMIT} ROWS ONLY;`;
case "rows":
return `SELECT *\nFROM table\nROWS ${DEFAULT_SELECT_ROW_LIMIT};`;
case "rownum":
return `SELECT *\nFROM table\nWHERE ROWNUM <= ${DEFAULT_SELECT_ROW_LIMIT};`;
case "unbounded":
return "SELECT *\nFROM table;";
case "limit":
return `SELECT *\nFROM table\nLIMIT ${DEFAULT_SELECT_ROW_LIMIT};`;
}
}
function buildUpdateSnippetBody(databaseType?: DatabaseType): string {
if (databaseType === "clickhouse") {
return "ALTER TABLE table\nUPDATE column = value\nWHERE condition;";
}
return "UPDATE table\nSET column = value\nWHERE condition;";
}
function buildAlterTableAddColumnSnippetBody(databaseType?: DatabaseType): string {
if (databaseType && PARENTHESIZED_ADD_COLUMN_DATABASES.has(databaseType)) {
return "ALTER TABLE table\nADD (column type);";
}
if (databaseType && ADD_COLUMN_WITHOUT_COLUMN_KEYWORD_DATABASES.has(databaseType)) {
return "ALTER TABLE table\nADD column type;";
}
return "ALTER TABLE table\nADD COLUMN column type;";
}
const BUILTIN_SQL_SNIPPET_RULES = new Map<string, BuiltinSqlSnippetRule>(
DEFAULT_SQL_SNIPPETS.map((snippet) => [
snippet.id,
{
defaultBody: snippet.body,
buildBody: () => snippet.body,
},
]),
);
function registerBuiltinSqlSnippetRule(id: string, buildBody: BuiltinSqlSnippetBodyBuilder): void {
const rule = BUILTIN_SQL_SNIPPET_RULES.get(id);
if (!rule) throw new Error(`Unknown built-in SQL snippet: ${id}`);
BUILTIN_SQL_SNIPPET_RULES.set(id, { defaultBody: rule.defaultBody, buildBody });
}
registerBuiltinSqlSnippetRule("builtin-sel", buildSelectSnippetBody);
registerBuiltinSqlSnippetRule("builtin-upd", buildUpdateSnippetBody);
registerBuiltinSqlSnippetRule("builtin-at", buildAlterTableAddColumnSnippetBody);
export function resolveSqlSnippetBodyForDatabase(snippet: SqlSnippet, databaseType?: DatabaseType): string {
const rule = BUILTIN_SQL_SNIPPET_RULES.get(snippet.id);
if (!rule || snippet.body !== rule.defaultBody) return snippet.body;
return rule.buildBody(databaseType);
}