diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index d03ee4ccff..90a7d0ac5b 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -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(["mysql", "clickhouse", "hive", "kyuubi", "impala", "spark", "databend", "tdengine", "access", "doris", "starrocks"]); +const POSTGRES_LIKE_IDENTIFIER_DATABASES = new Set(["postgres", "redshift", "gaussdb", "kingbase", "highgo", "uxdb", "vastbase", "kwdb", "opengauss"]); +const ORACLE_COMPAT_IDENTIFIER_DATABASES = new Set(["oracle", "oceanbase-oracle", "yashandb", "oscar", "xugu"]); +const UPPER_FOLDING_IDENTIFIER_DATABASES = new Set(["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>, @@ -3176,7 +3216,7 @@ function resolveTableSchemaQualification( function buildTableItems( context: Pick, 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, - 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, dialect?: "mysql" | "postgres" | "sqlserver", qualifierSql = context.qualifier, databaseType?: DatabaseType): string | null { +export function buildSelectStarExpansion(context: SqlCompletionContext, columnsByTable: Map, 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, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem | null { +function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map, 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, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem[] { +function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, 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, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] { +function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, 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, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] { +function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map, 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, foreignKeysByTable?: Map, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] { +function buildJoinConditionItems(context: SqlCompletionContext, columnsByTable: Map, foreignKeysByTable?: Map, 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, prefix = "", dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] { +function buildForeignKeyJoinConditionItemsForPair(left: SqlCompletionReferencedTable, right: SqlCompletionReferencedTable, foreignKeysByTable?: Map, 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, rightByName: Map, 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, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] { +function buildNonAggregatedColumnItems(context: SqlCompletionContext, columnsByTable: Map, dialect?: SqlCompletionApplyDialect): SqlCompletionItem[] { const nonAggSet = new Set(context.nonAggregatedSelectColumns.map((c) => c.toLowerCase())); const seen = new Set(); diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index fb668fb2a7..b0bec3814d 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -19,6 +19,7 @@ import { } from "../../apps/desktop/src/lib/sql/sqlCompletion.ts"; import { sqlCompletionContextFromSemantic } from "../../apps/desktop/src/lib/sql/semantic/completion.ts"; import { buildSqlSemanticModel } from "../../apps/desktop/src/lib/sql/semantic/model.ts"; +import type { DatabaseType } from "../../apps/desktop/src/types/database.ts"; const tables: SqlCompletionTable[] = [ { name: "users", schema: "public", type: "table" }, @@ -594,6 +595,106 @@ test("quotes MySQL-only reserved identifiers in schema, table, column, and join assert.equal(joinItems.find((item) => item.label === "a.accessible = m.accessible")?.apply, "a.`accessible` = m.`accessible`"); }); +test("does not reuse MySQL backticks for upper-folding schema and table completions", () => { + const databaseTypes: DatabaseType[] = ["oracle", "dameng", "oceanbase-oracle", "yashandb", "oscar", "xugu", "db2"]; + + for (const databaseType of databaseTypes) { + const schemaSql = "select * from SH"; + const schemaItems = buildSqlCompletionItems(schemaSql, schemaSql.length, { + tables: [], + columnsByTable: new Map(), + schemas: ["SHA"], + databaseType, + dialect: "mysql", + }); + assert.equal(schemaItems.find((item) => item.type === "schema" && item.label === "SHA")?.apply, "SHA.", databaseType); + + const tableSql = "select * from SHA.HOL"; + const tableItems = buildSqlCompletionItems(tableSql, tableSql.length, { + tables: [{ name: "HOLIDAYS", schema: "SHA", type: "table" }], + columnsByTable: new Map(), + databaseType, + dialect: "mysql", + }); + assert.equal(tableItems.find((item) => item.type === "table" && item.label === "HOLIDAYS")?.apply, "HOLIDAYS", databaseType); + } +}); + +test("uses double quotes for Oracle-like identifiers that require quoting", () => { + const schemaSql = "select * from Mixed"; + const schemaItems = buildSqlCompletionItems(schemaSql, schemaSql.length, { + tables: [], + columnsByTable: new Map(), + schemas: ["Mixed Schema"], + databaseType: "dameng", + dialect: "mysql", + }); + assert.equal(schemaItems.find((item) => item.type === "schema" && item.label === "Mixed Schema")?.apply, '"Mixed Schema".'); + + const columnSql = "select SEL from SHA.HOLIDAYS"; + const columnItems = buildSqlCompletionItems(columnSql, "select SEL".length, { + tables: [{ name: "HOLIDAYS", schema: "SHA", type: "table" }], + columnsByTable: new Map([["SHA.HOLIDAYS", [{ name: "SELECT", table: "HOLIDAYS", schema: "SHA" }]]]), + databaseType: "dameng", + dialect: "mysql", + }); + assert.equal(columnItems.find((item) => item.type === "column" && item.label === "SELECT")?.apply, '"SELECT"'); +}); + +test("quotes mixed-case Dameng schema, table, and column identifiers", () => { + const schemaSql = "select * from Mixed"; + const schemaItems = buildSqlCompletionItems(schemaSql, schemaSql.length, { + tables: [], + columnsByTable: new Map(), + schemas: ["MixedSchema"], + databaseType: "dameng", + dialect: "mysql", + }); + assert.equal(schemaItems.find((item) => item.type === "schema" && item.label === "MixedSchema")?.apply, '"MixedSchema".'); + + const tableSql = "select * from SHA.Mixed"; + const tableItems = buildSqlCompletionItems(tableSql, tableSql.length, { + tables: [{ name: "MixedTable", schema: "SHA", type: "table" }], + columnsByTable: new Map(), + databaseType: "dameng", + dialect: "mysql", + }); + assert.equal(tableItems.find((item) => item.type === "table" && item.label === "MixedTable")?.apply, '"MixedTable"'); + + const columnSql = "select Mixed from SHA.HOLIDAYS"; + const columnItems = buildSqlCompletionItems(columnSql, "select Mixed".length, { + tables: [{ name: "HOLIDAYS", schema: "SHA", type: "table" }], + columnsByTable: new Map([["SHA.HOLIDAYS", [{ name: "MixedColumn", table: "HOLIDAYS", schema: "SHA" }]]]), + databaseType: "dameng", + dialect: "mysql", + }); + assert.equal(columnItems.find((item) => item.type === "column" && item.label === "MixedColumn")?.apply, '"MixedColumn"'); +}); + +test("uses PostgreSQL-style double quotes for PostgreSQL-family completion identifiers", () => { + const databaseTypes: DatabaseType[] = ["kingbase", "vastbase", "highgo", "uxdb"]; + + for (const databaseType of databaseTypes) { + const reservedSql = "select * from SEL"; + const reservedItems = buildSqlCompletionItems(reservedSql, reservedSql.length, { + tables: [{ name: "SELECT", schema: "public", type: "table" }], + columnsByTable: new Map(), + databaseType, + dialect: "mysql", + }); + assert.equal(reservedItems.find((item) => item.type === "table" && item.label === "SELECT")?.apply, '"SELECT"', databaseType); + + const plainSql = "select * from ord"; + const plainItems = buildSqlCompletionItems(plainSql, plainSql.length, { + tables: [{ name: "orders", schema: "public", type: "table" }], + columnsByTable: new Map(), + databaseType, + dialect: "mysql", + }); + assert.equal(plainItems.find((item) => item.type === "table" && item.label === "orders")?.apply, "orders", databaseType); + } +}); + test("suggests matching table names after FROM", () => { const sql = "select * from us"; const items = buildSqlCompletionItems(sql, sql.length, {