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 43186403f8..bc44ed799b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.context.spec.ts @@ -446,4 +446,54 @@ describe("sqlCompletion scoped metadata ranking", () => { expect(items.findIndex((item) => item.label === "ORDERS_10K")).toBeLessThan(items.findIndex((item) => item.label === "TABLE")); }); + + it("qualifies same-name PostgreSQL tables from different schemas", () => { + const sql = "SELECT * FROM shared"; + const items = buildSqlCompletionItems(sql, sql.length, { + databaseType: "postgres", + dialect: "postgres", + tables: [ + { name: "shared", schema: "public", type: "table" }, + { name: "shared", schema: "reporting", type: "table" }, + ], + columnsByTable: new Map(), + }).filter((item) => item.type === "table"); + + expect(items).toHaveLength(2); + expect(items.map((item) => item.apply).sort()).toEqual(["public.shared", "reporting.shared"]); + }); + + it("qualifies same-name tables for generic metadata providers", () => { + const sql = "SELECT * FROM orders"; + const items = buildSqlCompletionItems(sql, sql.length, { + tables: [ + { name: "orders", schema: "archive", type: "table" }, + { name: "orders", schema: "sales", type: "table" }, + ], + columnsByTable: new Map(), + }).filter((item) => item.type === "table"); + + expect(items.map((item) => item.apply).sort()).toEqual(["archive.orders", "sales.orders"]); + }); + + it("preserves Oracle current-schema and SQL Server unique-table insertion", () => { + const oracleItems = buildSqlCompletionItems("SELECT * FROM ORDERS", "SELECT * FROM ORDERS".length, { + databaseType: "oracle", + tables: [ + { name: "ORDERS", schema: "APP", type: "table" }, + { name: "ORDERS", schema: "REPORTING", type: "table" }, + ], + columnsByTable: new Map(), + currentSchema: "APP", + }).filter((item) => item.type === "table"); + const sqlServerItems = buildSqlCompletionItems("SELECT * FROM Orders", "SELECT * FROM Orders".length, { + databaseType: "sqlserver", + dialect: "sqlserver", + tables: [{ name: "Orders", schema: "dbo", type: "table" }], + columnsByTable: new Map(), + }).filter((item) => item.type === "table"); + + expect(oracleItems.map((item) => item.apply).sort()).toEqual(["ORDERS", "REPORTING.ORDERS"]); + expect(sqlServerItems).toEqual([expect.objectContaining({ label: "Orders", apply: "Orders" })]); + }); }); diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index b5967dbecd..18b5ec969a 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -2958,13 +2958,26 @@ function buildTableItems( const { prefix } = context; const qualifierSchema = context.qualifier?.split(".").filter(Boolean).pop(); const existingAliases = new Set(referencedTables.map((ref) => ref.alias?.toLowerCase()).filter((alias): alias is string => !!alias)); - return tables - .filter((table) => matchesPrefix(table.name, prefix)) + const matchingTables = tables.filter((table) => matchesPrefix(table.name, prefix)); + const schemasByTableName = new Map>(); + for (const table of matchingTables) { + const tableName = normalizeIdentifierPart(table.name); + const schemas = schemasByTableName.get(tableName) ?? new Set(); + schemas.add(normalizeIdentifierPart(table.schema ?? "")); + schemasByTableName.set(tableName, schemas); + } + return matchingTables .map((table) => { const qualifiedByContext = !!qualifierSchema && !!table.schema && normalizeIdentifierPart(qualifierSchema) === normalizeIdentifierPart(table.schema); const oracleSchemaQualification = databaseType === "oracle" && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema)); - const defaultApplyName = oracleSchemaQualification ? `${quoteSqlIdentifier(table.schema!, dialect)}.${quoteSqlIdentifier(table.name, dialect)}` : quoteSqlIdentifier(table.name, dialect); - const applyName = qualifiedByContext ? quoteSqlIdentifier(table.name, dialect) : (table.applyName ?? defaultApplyName); + // A bare table name is ambiguous when metadata contains the same name in multiple schemas. + // Keep Oracle's current-schema behavior, but qualify the generic/PostgreSQL/SQL Server paths. + const ambiguousTableName = databaseType !== "oracle" && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1; + const schemaQualification = !!table.schema && (oracleSchemaQualification || ambiguousTableName); + const defaultApplyName = schemaQualification ? `${quoteSqlIdentifier(table.schema!, dialect)}.${quoteSqlIdentifier(table.name, dialect)}` : quoteSqlIdentifier(table.name, dialect); + const suppliedApplyName = table.applyName?.trim(); + const suppliedApplyNameIsQualified = suppliedApplyName?.includes(".") === true; + const applyName = qualifiedByContext ? quoteSqlIdentifier(table.name, dialect) : ambiguousTableName && !!table.schema && (!suppliedApplyName || !suppliedApplyNameIsQualified) ? defaultApplyName : (suppliedApplyName ?? defaultApplyName); const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : ""; return { label: table.name, @@ -2972,7 +2985,7 @@ function buildTableItems( detail: table.detail ?? (table.schema ? `${table.schema}.${table.name}` : table.type), apply: formatTableAliasApply(applyName, alias, databaseType), boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0), - dedupeKey: table.applyName || (databaseType === "oracle" && table.schema) ? applyName : undefined, + dedupeKey: table.applyName || ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined, }; }) .sort(compareCompletionItems)