fix(completion): apply database-specific identifier quoting

This commit is contained in:
zipg authored and GitHub committed 2026-08-16 14:16:04 +08:00
1 parent 2f3a32955c
commit 01ea889569
2 files changed
+171 -30

No files matched your search

+70 -30
View File
@@ -1220,6 +1220,26 @@ export function shouldChainSqlCompletionAfterAccept(item: { type?: string; apply
export type SqlKeywordCase = "preserve" | "upper" | "lower";
type SqlCompletionApplyDialect = "mysql" | "postgres" | "sqlserver" | "oracle" | "upper";
// QueryEditor may use another dialect as a CodeMirror syntax fallback.
// Completion apply text must still follow the connected database's identifier
// folding and quoting rules.
const MYSQL_LIKE_IDENTIFIER_DATABASES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "kyuubi", "impala", "spark", "databend", "tdengine", "access", "doris", "starrocks"]);
const POSTGRES_LIKE_IDENTIFIER_DATABASES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kingbase", "highgo", "uxdb", "vastbase", "kwdb", "opengauss"]);
const ORACLE_COMPAT_IDENTIFIER_DATABASES = new Set<DatabaseType>(["oracle", "oceanbase-oracle", "yashandb", "oscar", "xugu"]);
const UPPER_FOLDING_IDENTIFIER_DATABASES = new Set<DatabaseType>(["dameng", "db2"]);
function sqlCompletionApplyDialect(databaseType: DatabaseType | undefined, fallback: "mysql" | "postgres" | "sqlserver" | undefined): SqlCompletionApplyDialect | undefined {
if (!databaseType) return fallback;
if (MYSQL_LIKE_IDENTIFIER_DATABASES.has(databaseType)) return "mysql";
if (POSTGRES_LIKE_IDENTIFIER_DATABASES.has(databaseType)) return "postgres";
if (ORACLE_COMPAT_IDENTIFIER_DATABASES.has(databaseType)) return "oracle";
if (UPPER_FOLDING_IDENTIFIER_DATABASES.has(databaseType)) return "upper";
if (databaseType === "sqlserver") return "sqlserver";
return fallback;
}
export interface SqlCompletionReferencedTable {
name: string;
nameQuoted?: boolean;
@@ -1358,7 +1378,7 @@ export function buildSqlCompletionItemsFromContext(context: SqlCompletionContext
class SqlCompletionProvider {
private readonly items: SqlCompletionItem[] = [];
private readonly t?: SqlCompletionTranslations;
private readonly dialect?: "mysql" | "postgres" | "sqlserver";
private readonly dialect?: SqlCompletionApplyDialect;
private readonly databaseType?: DatabaseType;
constructor(
@@ -1366,7 +1386,7 @@ class SqlCompletionProvider {
private readonly input: SqlCompletionProviderInput,
) {
this.t = input.translations;
this.dialect = input.dialect;
this.dialect = sqlCompletionApplyDialect(input.databaseType, input.dialect);
this.databaseType = input.databaseType;
}
@@ -3096,7 +3116,15 @@ function unquoteIdentifier(value: string): string {
return value;
}
export function quoteSqlIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver"): string {
export function quoteSqlIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect === "oracle") {
if (/^[A-Za-z][A-Za-z0-9_$#]*$/.test(identifier) && !POSTGRES_IDENTIFIER_KEYWORDS.has(identifier.toLowerCase())) return identifier;
return `"${identifier.replaceAll('"', '""')}"`;
}
if (dialect === "upper") {
if (/^[A-Z][A-Z0-9_$#]*$/.test(identifier) && !POSTGRES_IDENTIFIER_KEYWORDS.has(identifier.toLowerCase())) return identifier;
return `"${identifier.replaceAll('"', '""')}"`;
}
if (dialect !== "postgres" || !requiresPostgresIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
return `"${identifier.replaceAll('"', '""')}"`;
}
@@ -3107,7 +3135,7 @@ const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keywo
// intentionally leaves MySQL identifiers unquoted and applies backticks itself
// at insertion time), SQL editor completion apply/insertText needs the quoting
// baked in up front, so MySQL reserved-word identifiers get backtick-quoted here.
function quoteCompletionApplyIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver"): string {
function quoteCompletionApplyIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect === "mysql") {
if (!requiresMysqlIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
return `\`${identifier.replaceAll("`", "``")}\``;
@@ -3115,14 +3143,26 @@ function quoteCompletionApplyIdentifier(identifier: string, dialect?: "mysql" |
return quoteSqlIdentifier(identifier, dialect);
}
function quoteCompletionApplyName(applyName: string, dialect?: "mysql" | "postgres" | "sqlserver"): string {
if (dialect !== "mysql") return applyName;
function quoteCompletionApplyName(applyName: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect !== "mysql" && dialect !== "oracle") return applyName;
const parts = splitQualifiedNameRawParts(applyName);
if (parts.length === 0) return applyName;
return parts.map((part) => (isQuotedIdentifier(part) ? part : quoteCompletionApplyIdentifier(part, dialect))).join(".");
}
function quoteSelectStarColumnIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): string {
function quoteCompletionRoutineIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect === "oracle" && /^[A-Za-z][A-Za-z0-9_$#]*$/.test(identifier) && !POSTGRES_IDENTIFIER_KEYWORDS.has(identifier.toLowerCase())) return identifier;
return quoteCompletionApplyIdentifier(identifier, dialect);
}
function quoteCompletionRoutineName(applyName: string, dialect?: SqlCompletionApplyDialect): string {
if (dialect !== "oracle") return quoteCompletionApplyName(applyName, dialect);
const parts = splitQualifiedNameRawParts(applyName);
if (parts.length === 0) return applyName;
return parts.map((part) => (isQuotedIdentifier(part) ? part : quoteCompletionRoutineIdentifier(part, dialect))).join(".");
}
function quoteSelectStarColumnIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType): string {
if (!requiresPostgresIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
if (databaseType) return quoteTableIdentifier(databaseType, identifier);
if (dialect === "mysql") return `\`${identifier.replaceAll("`", "``")}\``;
@@ -3159,7 +3199,7 @@ function collectSchemasByTableName(tables: SqlCompletionTable[]): Map<string, Se
*/
function resolveTableSchemaQualification(
table: SqlCompletionTable,
dialect: "mysql" | "postgres" | "sqlserver" | undefined,
dialect: SqlCompletionApplyDialect | undefined,
databaseType: DatabaseType | undefined,
currentSchema: string | undefined,
schemasByTableName: Map<string, Set<string>>,
@@ -3176,7 +3216,7 @@ function resolveTableSchemaQualification(
function buildTableItems(
context: Pick<SqlCompletionContext, "prefix" | "qualifier">,
tables: SqlCompletionTable[],
dialect?: "mysql" | "postgres" | "sqlserver",
dialect?: SqlCompletionApplyDialect,
autoAliasTables = false,
referencedTables: SqlCompletionReferencedTable[] = [],
databaseType?: DatabaseType,
@@ -3214,7 +3254,7 @@ function buildForeignKeyRelatedTableItems(
context: SqlCompletionContext,
tables: SqlCompletionTable[],
foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>,
dialect?: "mysql" | "postgres" | "sqlserver",
dialect?: SqlCompletionApplyDialect,
autoAliasTables = false,
databaseType?: DatabaseType,
keywordCase?: SqlKeywordCase,
@@ -3283,7 +3323,7 @@ function findCompletionTable(tables: SqlCompletionTable[], name: string, schema?
return tables.find((table) => normalizeIdentifierPart(table.name) === normalizedName && (!normalizedSchema || !table.schema || normalizeIdentifierPart(table.schema) === normalizedSchema));
}
function buildSchemaItems(prefix: string, schemas: string[], dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] {
function buildSchemaItems(prefix: string, schemas: string[], dialect?: SqlCompletionApplyDialect): SqlCompletionItem[] {
return schemas
.filter((schema) => matchesPrefix(schema, prefix))
.slice(0, 50)
@@ -3296,7 +3336,7 @@ function buildSchemaItems(prefix: string, schemas: string[], dialect?: "mysql" |
}));
}
function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionObject[], dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType, currentSchema?: string): SqlCompletionItem[] {
function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionObject[], dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType, currentSchema?: string): SqlCompletionItem[] {
if (completionQualifierIsReferencedTable(context)) return [];
const onlyProcedures = context.contextKind === "exec";
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
@@ -3306,11 +3346,11 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
.map((object) => {
const qualifiedByContext = objectIsQualifiedByContext(object, context);
const objectInCurrentSchema = !!currentSchema && !!object.schema && normalizeIdentifierPart(object.schema) === normalizeIdentifierPart(currentSchema);
const suppliedApplyName = object.applyName ? quoteCompletionApplyName(object.applyName, dialect) : undefined;
const suppliedApplyName = object.applyName ? quoteCompletionRoutineName(object.applyName, dialect) : undefined;
const applyName =
qualifiedByContext || (context.qualifier && object.schema?.toLowerCase() === context.qualifier.toLowerCase())
? quoteCompletionApplyIdentifier(object.name, dialect)
: (suppliedApplyName ?? (object.schema && !objectInCurrentSchema ? `${quoteCompletionApplyIdentifier(object.schema, dialect)}.${quoteCompletionApplyIdentifier(object.name, dialect)}` : quoteCompletionApplyIdentifier(object.name, dialect)));
? quoteCompletionRoutineIdentifier(object.name, dialect)
: (suppliedApplyName ?? (object.schema && !objectInCurrentSchema ? `${quoteCompletionRoutineIdentifier(object.schema, dialect)}.${quoteCompletionRoutineIdentifier(object.name, dialect)}` : quoteCompletionRoutineIdentifier(object.name, dialect)));
const locationDetail = object.type === "trigger" && object.parentName ? `trigger on ${object.parentName}` : object.parentName ? `${object.type} in ${object.parentName}` : object.schema ? `${object.type} in ${object.schema}` : object.type;
const signature = object.signature?.trim();
const detail = [locationDetail, signature ? `(${signature})` : undefined, object.dataType ? `[${object.dataType}]` : undefined].filter(Boolean).join(" ");
@@ -3513,7 +3553,7 @@ export function selectStarResultColumnsMatch(options: { currentSql: string; targ
return options.targetFrom >= options.sourceFrom! && options.targetTo <= options.sourceTo! && sourceToAtBoundary && options.currentSql.slice(options.sourceFrom, options.sourceTo) === options.sourceStatement;
}
export function buildSelectStarExpansion(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: "mysql" | "postgres" | "sqlserver", qualifierSql = context.qualifier, databaseType?: DatabaseType): string | null {
export function buildSelectStarExpansion(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: SqlCompletionApplyDialect, qualifierSql = context.qualifier, databaseType?: DatabaseType): string | null {
const columns = selectStarExpansionColumns(context, columnsByTable);
if (columns.length === 0) return null;
// `alias.*` replaces only the `*`, so the first column must continue the already typed `alias.`.
@@ -3530,7 +3570,7 @@ export function buildSelectStarExpansion(context: SqlCompletionContext, columnsB
.join(", ");
}
function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem | null {
function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType): SqlCompletionItem | null {
const expansion = buildSelectStarExpansion(context, columnsByTable, dialect, context.qualifier, databaseType);
if (!expansion) return null;
const columnCount = selectStarExpansionColumns(context, columnsByTable).length;
@@ -3543,7 +3583,7 @@ function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: M
};
}
function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem[] {
function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType): SqlCompletionItem[] {
if (!context.selectListColumnContext || context.statementKind !== "select" || context.onStar || context.referencedTables.length === 0) {
return [];
}
@@ -3580,7 +3620,7 @@ function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable
return items;
}
function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: SqlCompletionApplyDialect, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
if (!context.insertTable) return [];
const columns = uniqueColumnsByName(columnsForInsertTarget(context, columnsByTable));
if (columns.length === 0) return [];
@@ -3613,7 +3653,7 @@ function referencedTablesForSelectAllColumns(context: SqlCompletionContext): Sql
return context.referencedTables.filter((table) => referencedTableMatchesColumnQualifier(table, qualifier, qualifierLower, qualifiedTarget));
}
function buildSelectAllColumnExpansion(columns: SqlCompletionColumn[], qualifier: string | undefined, qualifierAlreadyTyped: boolean, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): string {
function buildSelectAllColumnExpansion(columns: SqlCompletionColumn[], qualifier: string | undefined, qualifierAlreadyTyped: boolean, dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType): string {
return columns
.map((column, index) => {
const columnName = quoteSelectStarColumnIdentifier(column.name, dialect, databaseType);
@@ -3940,7 +3980,7 @@ function columnsForInsertTarget(context: SqlCompletionContext, columnsByTable: M
});
}
function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] {
function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: SqlCompletionApplyDialect): SqlCompletionItem[] {
// Collect all columns from the map (all tables have been fetched)
const allColumns = collectCompletionColumns(columnsByTable);
@@ -4072,7 +4112,7 @@ function normalizeCompletionKey(key: string): string {
.join(".");
}
function buildColumnApply(column: SqlCompletionColumn & { displayLabel: string }, context: SqlCompletionContext, dialect?: "mysql" | "postgres" | "sqlserver"): string {
function buildColumnApply(column: SqlCompletionColumn & { displayLabel: string }, context: SqlCompletionContext, dialect?: SqlCompletionApplyDialect): string {
if (context.qualifier || column.displayLabel === column.name || !column.displayLabel.includes(".")) {
return quoteCompletionApplyIdentifier(column.name, dialect);
}
@@ -4108,7 +4148,7 @@ function buildColumnInfo(column: SqlCompletionColumn): string | undefined {
return parts.length > 1 ? parts.join("\n") : undefined;
}
function buildJoinConditionItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildJoinConditionItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>, dialect?: SqlCompletionApplyDialect, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
const refs = context.referencedTables;
if (refs.length < 2) return [];
@@ -4144,7 +4184,7 @@ function foreignKeysForReferencedTable(table: SqlCompletionReferencedTable, fore
return [];
}
function buildForeignKeyJoinConditionItemsForPair(left: SqlCompletionReferencedTable, right: SqlCompletionReferencedTable, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>, prefix = "", dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildForeignKeyJoinConditionItemsForPair(left: SqlCompletionReferencedTable, right: SqlCompletionReferencedTable, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>, prefix = "", dialect?: SqlCompletionApplyDialect, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
if (!foreignKeysByTable) return [];
return [
...buildDirectionalForeignKeyJoinConditionItems(left, right, foreignKeysForReferencedTable(left, foreignKeysByTable), prefix, dialect, keywordCase),
@@ -4152,7 +4192,7 @@ function buildForeignKeyJoinConditionItemsForPair(left: SqlCompletionReferencedT
];
}
function buildDirectionalForeignKeyJoinConditionItems(owner: SqlCompletionReferencedTable, referenced: SqlCompletionReferencedTable, foreignKeys: SqlCompletionForeignKey[], prefix: string, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildDirectionalForeignKeyJoinConditionItems(owner: SqlCompletionReferencedTable, referenced: SqlCompletionReferencedTable, foreignKeys: SqlCompletionForeignKey[], prefix: string, dialect?: SqlCompletionApplyDialect, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
const matchingForeignKeys = foreignKeys.filter((foreignKey) => referencedTableMatchesName(referenced, foreignKey.ref_table, foreignKey.ref_schema));
const groups = groupForeignKeysByConstraint(matchingForeignKeys);
const items: SqlCompletionItem[] = [];
@@ -4175,7 +4215,7 @@ function buildDirectionalForeignKeyJoinConditionItems(owner: SqlCompletionRefere
return items;
}
function buildJoinConditionPart(owner: SqlCompletionReferencedTable, ownerColumn: string, referenced: SqlCompletionReferencedTable, referencedColumn: string, dialect?: "mysql" | "postgres" | "sqlserver"): { label: string; apply: string } {
function buildJoinConditionPart(owner: SqlCompletionReferencedTable, ownerColumn: string, referenced: SqlCompletionReferencedTable, referencedColumn: string, dialect?: SqlCompletionApplyDialect): { label: string; apply: string } {
const ownerRef = owner.alias || owner.name;
const referencedRef = referenced.alias || referenced.name;
const ownerApplyRef = owner.alias ? owner.alias : quoteCompletionApplyIdentifier(owner.name, dialect);
@@ -4216,7 +4256,7 @@ function normalizeIdentifierPart(name: string): string {
return name.replace(/^["`[]|["`\]]$/g, "").toLowerCase();
}
function buildJoinConditionItemsForPair(left: SqlCompletionReferencedTable, leftColumns: SqlCompletionColumn[], right: SqlCompletionReferencedTable, rightColumns: SqlCompletionColumn[], prefix: string, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
function buildJoinConditionItemsForPair(left: SqlCompletionReferencedTable, leftColumns: SqlCompletionColumn[], right: SqlCompletionReferencedTable, rightColumns: SqlCompletionColumn[], prefix: string, dialect?: SqlCompletionApplyDialect, keywordCase?: SqlKeywordCase): SqlCompletionItem[] {
const items: SqlCompletionItem[] = [];
const leftRef = left.alias || left.name;
const rightRef = right.alias || right.name;
@@ -4307,7 +4347,7 @@ function buildCompositeHeuristicJoinConditionItems(
leftByName: Map<string, SqlCompletionColumn[]>,
rightByName: Map<string, SqlCompletionColumn[]>,
prefix: string,
dialect?: "mysql" | "postgres" | "sqlserver",
dialect?: SqlCompletionApplyDialect,
keywordCase?: SqlKeywordCase,
): SqlCompletionItem[] {
const leftId = leftByName.get("id")?.[0];
@@ -4366,7 +4406,7 @@ function buildCompositeHeuristicJoinConditionItems(
return items;
}
function buildHeuristicJoinConditionPart(leftRef: string, leftApplyRef: string, leftColumn: SqlCompletionColumn, rightRef: string, rightApplyRef: string, rightColumn: SqlCompletionColumn, dialect?: "mysql" | "postgres" | "sqlserver"): { label: string; apply: string } {
function buildHeuristicJoinConditionPart(leftRef: string, leftApplyRef: string, leftColumn: SqlCompletionColumn, rightRef: string, rightApplyRef: string, rightColumn: SqlCompletionColumn, dialect?: SqlCompletionApplyDialect): { label: string; apply: string } {
return {
label: `${leftRef}.${leftColumn.name} = ${rightRef}.${rightColumn.name}`,
apply: `${leftApplyRef}.${quoteCompletionApplyIdentifier(leftColumn.name, dialect)} = ${rightApplyRef}.${quoteCompletionApplyIdentifier(rightColumn.name, dialect)}`,
@@ -4584,7 +4624,7 @@ function buildSelectAliasItems(context: SqlCompletionContext): SqlCompletionItem
}));
}
function buildNonAggregatedColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] {
function buildNonAggregatedColumnItems(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: SqlCompletionApplyDialect): SqlCompletionItem[] {
const nonAggSet = new Set(context.nonAggregatedSelectColumns.map((c) => c.toLowerCase()));
const seen = new Set<string>();