diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index ffa77bd2f1..d46807a54f 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -475,13 +475,13 @@ FROM ( }); it.each([ - ["Oracle", "oracle", "mysql", '"ID", o."created at", o."SELECT", o.safe_name'], - ["MySQL", "mysql", "mysql", "`ID`, o.`created at`, o.`SELECT`, o.safe_name"], - ["PostgreSQL", "postgres", "postgres", '"ID", o."created at", o."SELECT", o.safe_name'], - ["SQL Server", "sqlserver", "sqlserver", "[ID], o.[created at], o.[SELECT], o.safe_name"], - ["dialect fallback", undefined, "mysql", "`ID`, o.`created at`, o.`SELECT`, o.safe_name"], + ["Oracle", "oracle", "mysql", 'ID, o."created at", o."SELECT", o."safe_name", o."OrderId", o."order_id"'], + ["MySQL", "mysql", "mysql", "`ID`, o.`created at`, o.`SELECT`, o.safe_name, o.`OrderId`, o.order_id"], + ["PostgreSQL", "postgres", "postgres", '"ID", o."created at", o."SELECT", o.safe_name, o."OrderId", o.order_id'], + ["SQL Server", "sqlserver", "sqlserver", "[ID], o.[created at], o.[SELECT], o.safe_name, o.[OrderId], o.order_id"], + ["dialect fallback", undefined, "mysql", "`ID`, o.`created at`, o.`SELECT`, o.safe_name, o.`OrderId`, o.order_id"], ] as const)("uses %s identifier quoting in qualified star completion items", (_label, databaseType, dialect, expected) => { - const columnsByTable = new Map([["orders", ["ID", "created at", "SELECT", "safe_name"].map((name) => ({ name, table: "orders" }))]]); + const columnsByTable = new Map([["orders", ["ID", "created at", "SELECT", "safe_name", "OrderId", "order_id"].map((name) => ({ name, table: "orders" }))]]); const starItems = semanticCompletion("SELECT o.*| FROM orders o", { columnsByTable }, { databaseType, dialect }).items; const selectAllItems = semanticCompletion("SELECT o.| FROM orders o", { columnsByTable }, { databaseType, dialect }).items; @@ -492,13 +492,13 @@ FROM ( it("uses Oracle quoting for an unqualified multi-table star completion item", () => { const columnsByTable = new Map([ - ["ORDERS", ["ID", "created at"].map((name) => ({ name, table: "ORDERS" }))], - ["AUDIT", ["ID", "SELECT"].map((name) => ({ name, table: "AUDIT" }))], + ["ORDERS", ["ID", "created at", "OrderId"].map((name) => ({ name, table: "ORDERS" }))], + ["AUDIT", ["ID", "SELECT", "order_id"].map((name) => ({ name, table: "AUDIT" }))], ]); const { items } = semanticCompletion("SELECT *| FROM ORDERS o JOIN AUDIT a ON a.ID = o.ID", { columnsByTable }, { databaseType: "oracle", dialect: "mysql" }); - expect(items.find((item) => item.label === "* \u2192 columns")?.apply).toBe('o."ID", o."created at", a."ID", a."SELECT"'); + expect(items.find((item) => item.label === "* \u2192 columns")?.apply).toBe('o.ID, o."created at", o."OrderId", a.ID, a."SELECT", a."order_id"'); }); it("generates collision-free table aliases from semantic row sources", () => { diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts index 06ce223ffa..a5ccc9a21b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -334,7 +334,76 @@ describe("SELECT star expansion", () => { qualifierSql, databaseType, ), - ).toBe(`id, ${qualifierSql}.${quotedColumn}`); + ).toBe(`${databaseType === "oracle" ? '"id"' : "id"}, ${qualifierSql}.${quotedColumn}`); + }); + + it("does not quote all-uppercase Oracle columns when expanding alias.* (regression #9163)", () => { + const sql = "SELECT t.* FROM device_table AS t"; + const cursor = sql.indexOf("*") + 1; + const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor, { databaseType: "oracle", dialect: "mysql" }), getSqlCompletionContext(sql, cursor, { databaseType: "oracle", dialect: "mysql" })); + + expect( + buildSelectStarExpansion( + context, + new Map([ + [ + "device_table", + [ + { name: "AGENT_NAME", table: "device_table" }, + { name: "PROTOCOL", table: "device_table" }, + { name: "created at", table: "device_table" }, + ], + ], + ]), + "mysql", + "t", + "oracle", + ), + ).toBe('AGENT_NAME, t.PROTOCOL, t."created at"'); + }); + + it("does not quote all-uppercase Oracle columns for an unqualified star when only databaseType is set", () => { + const sql = "SELECT * FROM device_table"; + const cursor = "SELECT *".length; + const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor, { databaseType: "oracle" }), getSqlCompletionContext(sql, cursor, { databaseType: "oracle" })); + + expect( + buildSelectStarExpansion( + context, + new Map([ + [ + "device_table", + [ + { name: "AGENT_NAME", table: "device_table" }, + { name: "SPARE1", table: "device_table" }, + ], + ], + ]), + undefined, + undefined, + "oracle", + ), + ).toBe("AGENT_NAME, SPARE1"); + }); + + it.each([ + ["oracle", "mysql"], + ["oracle", undefined], + [undefined, "oracle"], + ] as const)("preserves required Oracle column quotes with databaseType %s and dialect %s", (databaseType, dialect) => { + const columnsByTable = new Map([["orders", ["OrderId", "order_id", "AGENT_NAME", "SELECT", "created at", "ACCOUNT$SYS"].map((name) => ({ name, table: "orders" }))]]); + const contextOptions = { databaseType, dialect: "mysql" as const }; + + for (const [sql, expected] of [ + ["SELECT o.* FROM orders o", '"OrderId", o."order_id", o.AGENT_NAME, o."SELECT", o."created at", o.ACCOUNT$SYS'], + ["SELECT * FROM orders", '"OrderId", "order_id", AGENT_NAME, "SELECT", "created at", ACCOUNT$SYS'], + ]) { + const cursor = sql.indexOf("*") + 1; + const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor, contextOptions), getSqlCompletionContext(sql, cursor, contextOptions)); + + expect(buildSelectStarExpansion(context, columnsByTable, dialect, context.qualifier, databaseType)).toBe(expected); + expect(buildSelectStarExpansion(context, new Map(), dialect, context.qualifier, databaseType)).toBeNull(); + } }); it("expands an unqualified star from result columns when the table has an alias", () => { diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 5f205c230f..c51f686317 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -16,7 +16,7 @@ import { BACKSLASH_ESCAPE_STRING_DIALECTS } from "@/lib/sql/sqlStatementRanges"; import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier"; import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identifierSearch"; import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin"; -import { quoteTableIdentifier } from "@/lib/table/tableSelectSql"; +import { quoteTableIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/tableSelectSql"; import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions"; import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions"; import { rejectsAliasReferenceInHaving } from "@/lib/database/databaseFeatureSupport"; @@ -3760,6 +3760,7 @@ function quoteCompletionRoutineName(applyName: string, dialect?: SqlCompletionAp } function quoteSelectStarColumnIdentifier(identifier: string, dialect?: SqlCompletionApplyDialect, databaseType?: DatabaseType): string { + if (databaseType === "oracle" || (databaseType === undefined && dialect === "oracle")) return quoteTableIdentifierIfNeeded("oracle", identifier); if (!requiresPostgresIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier; if (databaseType) return quoteTableIdentifier(databaseType, identifier); if (dialect === "mysql") return `\`${identifier.replaceAll("`", "``")}\``;