fix(editor): improve Oracle SQL completion

This commit is contained in:
zipg 2026-07-13 17:29:56 +08:00 committed by GitHub
parent 8c11f2efaa
commit 00101ff0ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 155 additions and 3 deletions

View File

@ -465,6 +465,74 @@ const SQLSERVER_SQL_KEYWORDS = [
"SHOWPLAN_XML",
];
function sqlDialectCompletionWords(...sources: Array<string | undefined>): string[] {
return sources
.flatMap((source) => (source ?? "").split(/\s+/))
.filter((keyword) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(keyword))
.map((keyword) => keyword.toUpperCase());
}
const ORACLE_SQL_TYPES = [
"BFILE",
"BINARY_DOUBLE",
"BINARY_FLOAT",
"BLOB",
"CHAR",
"CLOB",
"DATE",
"DEC",
"DECIMAL",
"DOUBLE PRECISION",
"FLOAT",
"INT",
"INTEGER",
"INTERVAL DAY TO SECOND",
"INTERVAL YEAR TO MONTH",
"LONG",
"LONG RAW",
"NCHAR",
"NCLOB",
"NUMBER",
"NUMERIC",
"NVARCHAR2",
"RAW",
"REAL",
"ROWID",
"SMALLINT",
"TIMESTAMP",
"TIMESTAMP WITH LOCAL TIME ZONE",
"TIMESTAMP WITH TIME ZONE",
"UROWID",
"VARCHAR",
"VARCHAR2",
"XMLTYPE",
];
const NON_ORACLE_COMPLETION_WORDS = new Set(["BIGSERIAL", "BOOLEAN", "ELSEIF", "LIMIT", "LOCALTIME", "SERIAL", "STRING", "TEXT", "TIME", "USE"]);
const ORACLE_SQL_KEYWORDS = Array.from(
new Set([
...sqlDialectCompletionWords(PLSQL.spec.keywords).filter((keyword) => !NON_ORACLE_COMPLETION_WORDS.has(keyword)),
...ORACLE_SQL_TYPES,
"BULK COLLECT",
"CONNECT BY",
"DATABASE LINK",
"EXECUTE IMMEDIATE",
"FLASHBACK",
"FOR UPDATE",
"MATERIALIZED VIEW",
"MERGE",
"ORDER SIBLINGS BY",
"OR REPLACE",
"PACKAGE BODY",
"PURGE",
"RETURNING INTO",
"SEQUENCE",
"START WITH",
"TYPE BODY",
]),
);
const DATABASE_SQL_KEYWORDS: Partial<Record<DatabaseType, string[]>> = {
mysql: MYSQL_SQL_KEYWORDS,
postgres: POSTGRES_SQL_KEYWORDS,
@ -473,6 +541,8 @@ const DATABASE_SQL_KEYWORDS: Partial<Record<DatabaseType, string[]>> = {
turso: SQLITE_SQL_KEYWORDS,
"cloudflare-d1": SQLITE_SQL_KEYWORDS,
sqlserver: SQLSERVER_SQL_KEYWORDS,
oracle: ORACLE_SQL_KEYWORDS,
"oceanbase-oracle": ORACLE_SQL_KEYWORDS,
manticoresearch: MANTICORESEARCH_SQL_KEYWORDS,
};
@ -614,6 +684,7 @@ const DATA_TYPE_KEYWORDS = new Set([
"TINYBLOB",
"MEDIUMBLOB",
"LONGBLOB",
...ORACLE_SQL_TYPES,
]);
// Window functions that should use OVER() completion
@ -3826,7 +3897,8 @@ function buildNonAggregatedColumnItems(context: SqlCompletionContext, columnsByT
function activeSqlKeywords(databaseType?: DatabaseType): string[] {
if (databaseType === "mongodb") return [];
const databaseKeywords = databaseType ? DATABASE_SQL_KEYWORDS[databaseType] : undefined;
return databaseType ? Array.from(new Set([...COMMON_SQL_KEYWORDS, ...(databaseKeywords ?? [])])) : Array.from(new Set(SQL_KEYWORDS));
const keywords = databaseType ? Array.from(new Set([...COMMON_SQL_KEYWORDS, ...(databaseKeywords ?? [])])) : Array.from(new Set(SQL_KEYWORDS));
return isOracleLikeDatabase(databaseType) ? keywords.filter((keyword) => !NON_ORACLE_COMPLETION_WORDS.has(keyword)) : keywords;
}
function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
@ -3854,11 +3926,11 @@ function isPendingJoinKeywordContext(context: SqlCompletionContext): boolean {
function buildKeywordItems(prefix: string, context: SqlCompletionContext, databaseType?: DatabaseType, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
const isDml = context.statementKind === "select" || context.statementKind === "insert" || context.statementKind === "update" || context.statementKind === "delete";
const showDdl = !isDml || context.suggestTables;
const functionSignatures = activeFunctionSignatures(databaseType);
return activeSqlKeywords(databaseType)
.filter((keyword) => {
if (SQL_FUNCTION_SIGNATURES.has(keyword)) return false;
if (databaseType && DATABASE_FUNCTION_SIGNATURES[databaseType]?.has(keyword)) return false;
if (functionSignatures.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;

View File

@ -144,6 +144,86 @@ test("suggests PostgreSQL-specific data types and functions", () => {
);
});
test("suggests Oracle SQL, PL/SQL, and data type keywords", () => {
const keywordCases = [
["tru", "TRUNCATE"],
["mer", "MERGE"],
["dec", "DECLARE"],
["els", "ELSIF"],
["pac", "PACKAGE"],
["seq", "SEQUENCE"],
["flash", "FLASHBACK"],
["mat", "MATERIALIZED VIEW"],
] as const;
for (const [prefix, expected] of keywordCases) {
const items = buildSqlCompletionItems(prefix, prefix.length, {
tables: [],
columnsByTable: new Map(),
databaseType: "oracle",
});
assert.ok(
items.some((item) => item.type === "keyword" && item.label === expected),
`${expected} should be suggested for ${prefix}`,
);
}
const typeSql = "CREATE TABLE events (payload varc";
const typeItems = buildSqlCompletionItems(typeSql, typeSql.length, {
tables: [],
columnsByTable: new Map(),
databaseType: "oracle",
});
assert.ok(typeItems.some((item) => item.type === "keyword" && item.label === "VARCHAR2"));
});
test("does not suggest cross-dialect words for Oracle", () => {
const unsupportedWords = ["LIMIT", "LOCALTIME", "USE", "ELSEIF", "SERIAL", "BIGSERIAL", "TEXT", "BOOLEAN", "STRING", "TIME"];
for (const unsupportedWord of unsupportedWords) {
const prefix = unsupportedWord.toLowerCase();
const items = buildSqlCompletionItems(prefix, prefix.length, {
tables: [],
columnsByTable: new Map(),
databaseType: "oracle",
});
assert.equal(
items.some((item) => item.type === "keyword" && item.label === unsupportedWord),
false,
`${unsupportedWord} should not be suggested for Oracle`,
);
}
for (const supportedWord of ["LOCALTIMESTAMP", "NUMBER", "VARCHAR2", "XMLTYPE"]) {
const prefix = supportedWord.toLowerCase();
const items = buildSqlCompletionItems(prefix, prefix.length, {
tables: [],
columnsByTable: new Map(),
databaseType: "oracle",
});
assert.ok(
items.some((item) => item.type === "keyword" && item.label === supportedWord),
`${supportedWord} should be suggested for Oracle`,
);
}
});
test("keeps TRUNCATE as a statement keyword for Oracle-compatible databases", () => {
for (const databaseType of ["oracle", "oceanbase-oracle"] as const) {
const items = buildSqlCompletionItems("tru", 3, {
tables: [],
columnsByTable: new Map(),
databaseType,
});
assert.ok(items.some((item) => item.type === "keyword" && item.label === "TRUNCATE"));
assert.equal(
items.some((item) => item.type === "function" && item.label === "TRUNCATE"),
false,
);
}
});
test("suggests Manticore Search SQL functions and command snippets", () => {
const matchItems = buildSqlCompletionItems("select * from products where mat", "select * from products where mat".length, {
tables,