fix(sql): preserve Oracle identifier semantics in star expansion

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-18 20:06:33 +08:00
1 parent 3597a00759
commit aa4ed91c40
3 files changed
+81 -11

No files matched your search

@@ -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<string, SqlCompletionColumn[]>([["orders", ["ID", "created at", "SELECT", "safe_name"].map((name) => ({ name, table: "orders" }))]]);
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["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<string, SqlCompletionColumn[]>([
["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", () => {
@@ -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", () => {
+2 -1
View File
@@ -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("`", "``")}\``;