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 39102a7eca..600dc1c236 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -417,6 +417,33 @@ WHERE a.id = b.fk_kpi_set_score_id`, expect(star?.apply).toBe("id, u.name"); }); + 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"], + ] 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 starItems = semanticCompletion("SELECT o.*| FROM orders o", { columnsByTable }, { databaseType, dialect }).items; + const selectAllItems = semanticCompletion("SELECT o.| FROM orders o", { columnsByTable }, { databaseType, dialect }).items; + + expect(starItems.find((item) => item.label === "* \u2192 columns")?.apply).toBe(expected); + expect(selectAllItems.find((item) => item.label === "o.*")?.apply).toBe(expected); + }); + + 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" }))], + ]); + + 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"'); + }); + it("generates collision-free table aliases from semantic row sources", () => { const { items } = semanticCompletion("SELECT * FROM order_items oi JOIN ord|", { tables: [{ name: "order_items", type: "table" }], diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 193b431e12..beff553317 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -1433,7 +1433,7 @@ class SqlCompletionProvider { if (!context.exclusiveTableSuggestions && context.suggestColumns) { this.items.push(...buildColumnItems(context, this.input.columnsByTable, this.dialect)); - this.items.push(...buildSelectAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect)); + this.items.push(...buildSelectAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.databaseType)); this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.input.keywordCase)); } @@ -1466,7 +1466,7 @@ class SqlCompletionProvider { } if (context.onStar) { - const starItem = buildStarExpansionItem(context, this.input.columnsByTable, this.t, this.dialect); + const starItem = buildStarExpansionItem(context, this.input.columnsByTable, this.t, this.dialect, this.databaseType); if (starItem) this.items.push(starItem); } @@ -3518,8 +3518,8 @@ export function buildSelectStarExpansion(context: SqlCompletionContext, columnsB .join(", "); } -function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem | null { - const expansion = buildSelectStarExpansion(context, columnsByTable, dialect); +function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem | null { + const expansion = buildSelectStarExpansion(context, columnsByTable, dialect, context.qualifier, databaseType); if (!expansion) return null; const columnCount = selectStarExpansionColumns(context, columnsByTable).length; return { @@ -3531,7 +3531,7 @@ function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: M }; } -function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] { +function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): SqlCompletionItem[] { if (!context.selectListColumnContext || context.statementKind !== "select" || context.onStar || context.referencedTables.length === 0) { return []; } @@ -3554,7 +3554,7 @@ function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable if (!selectAllColumnItemMatchesPrefix(label, ref, columns, context.prefix)) continue; const qualifier = context.qualifier || ref.alias || (shouldQualify ? quoteCompletionApplyIdentifier(ref.name, dialect) : undefined); - const expansion = buildSelectAllColumnExpansion(columns, qualifier, !!context.qualifier, dialect); + const expansion = buildSelectAllColumnExpansion(columns, qualifier, !!context.qualifier, dialect, databaseType); const countText = (t?.starExpansionColumns ?? "{count} columns").replace("{count}", String(columns.length)); items.push({ label,