diff --git a/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts b/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts index 3305d279..bb9aa44b 100644 --- a/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts +++ b/frontend/src/components/sidebar/sidebarMetadataLoaders.test.ts @@ -10,6 +10,7 @@ import { buildPackagesMetadataQuerySpecs, buildSchemasMetadataQuerySpecs, buildSequencesMetadataQuerySpecs, + buildViewsMetadataQuerySpecs, loadFunctions, loadPackages, loadSequences, @@ -78,8 +79,15 @@ describe("buildSchemasMetadataQuerySpecs", () => { }); describe("Oracle object metadata loaders", () => { - it("builds owner-scoped sequence and package queries for Oracle", () => { + it("builds owner-scoped object queries for the selected Oracle schema", () => { + expect(buildViewsMetadataQuerySpecs("oracle", "SBDEV").map((spec) => spec.sql)).toEqual([ + "SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = 'SBDEV' ORDER BY VIEW_NAME", + ]); + expect(buildFunctionsMetadataQuerySpecs("oracle", "SBDEV").map((spec) => spec.sql)).toEqual([ + "SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = 'SBDEV' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME", + ]); expect(buildSequencesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([ + "SELECT OWNER AS schema_name, OBJECT_NAME AS sequence_name FROM ALL_OBJECTS WHERE OWNER = 'MYCIMLED' AND OBJECT_TYPE = 'SEQUENCE' ORDER BY OBJECT_NAME", "SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = 'MYCIMLED' ORDER BY SEQUENCE_NAME", ]); expect(buildPackagesMetadataQuerySpecs("oracle", "MYCIMLED").map((spec) => spec.sql)).toEqual([ @@ -133,6 +141,54 @@ describe("Oracle object metadata loaders", () => { ], }); }); + + it("uses the selected owner catalog for OceanBase Oracle read-only connections", async () => { + const executedSql: string[] = []; + mockedDBQuery.mockImplementation(async (_config: unknown, _dbName: string, sql: string) => { + executedSql.push(sql); + if (sql.includes("ALL_VIEWS") && sql.includes("OWNER = 'SBDEV'")) { + return { + success: true, + message: "", + data: [{ OWNER: "SBDEV", VIEW_NAME: "V_RISK" }], + }; + } + if (sql.includes("ALL_OBJECTS") && sql.includes("('FUNCTION','PROCEDURE')")) { + return { + success: true, + message: "", + data: [{ OWNER: "SBDEV", OBJECT_NAME: "P_REFRESH", OBJECT_TYPE: "PROCEDURE" }], + }; + } + if (sql.includes("ALL_OBJECTS") && sql.includes("OBJECT_TYPE = 'SEQUENCE'")) { + return { + success: true, + message: "", + data: [{ OWNER: "SBDEV", OBJECT_NAME: "SEQ_RISK" }], + }; + } + return { success: false, message: "", data: [] }; + }); + + const conn = { config: { type: "oceanbase", oceanBaseProtocol: "oracle" } }; + + await expect(loadViews(conn, "SBDEV")).resolves.toEqual({ + supported: true, + views: [{ schemaName: "SBDEV", viewName: "SBDEV.V_RISK" }], + }); + await expect(loadFunctions(conn, "SBDEV")).resolves.toEqual({ + supported: true, + routines: [{ displayName: "SBDEV.P_REFRESH [P]", routineName: "SBDEV.P_REFRESH", routineType: "PROCEDURE" }], + }); + await expect(loadSequences(conn, "SBDEV")).resolves.toEqual({ + supported: true, + sequences: [{ displayName: "SBDEV.SEQ_RISK", schemaName: "SBDEV", sequenceName: "SBDEV.SEQ_RISK" }], + }); + + expect(executedSql).toHaveLength(3); + expect(executedSql).not.toContain(expect.stringContaining("USER_")); + expect(executedSql).not.toContain(expect.stringContaining("ALL_SEQUENCES")); + }); }); describe("Kingbase/PG routine metadata loaders", () => { diff --git a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts index 0f8fde7e..24ab3231 100644 --- a/frontend/src/components/sidebar/sidebarMetadataLoaders.ts +++ b/frontend/src/components/sidebar/sidebarMetadataLoaders.ts @@ -435,6 +435,17 @@ const buildViewsMetadataQuerySpecs = ( } case "oracle": case "dm": + if (safeDbName) { + // CURRENT_SCHEMA does not change USER. When a read-only account selects + // another Oracle schema, USER_VIEWS would successfully return that + // account's (usually empty) catalog and prevent the target schema from + // being queried. Read the explicitly selected owner first instead. + return [ + { + sql: `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME`, + }, + ]; + } return normalizeMetadataQuerySpecs([ { sql: `SELECT VIEW_NAME AS view_name FROM USER_VIEWS ORDER BY VIEW_NAME`, @@ -442,11 +453,6 @@ const buildViewsMetadataQuerySpecs = ( { sql: `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = USER ORDER BY VIEW_NAME`, }, - { - sql: safeDbName - ? `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME` - : "", - }, ]); case "sqlite": return [ @@ -589,6 +595,16 @@ const buildFunctionsMetadataQuerySpecs = ( } case "oracle": case "dm": + if (safeDbName) { + // See the corresponding view query above. Oracle CURRENT_SCHEMA only + // changes name resolution, so USER_OBJECTS still belongs to the login + // account rather than the schema selected in the sidebar. + return [ + { + sql: `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`, + }, + ]; + } return normalizeMetadataQuerySpecs([ { sql: `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`, @@ -596,11 +612,6 @@ const buildFunctionsMetadataQuerySpecs = ( { sql: `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = USER AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`, }, - { - sql: safeDbName - ? `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` - : "", - }, ]); case "duckdb": return [ @@ -625,9 +636,17 @@ const buildSequencesMetadataQuerySpecs = ( return normalizeMetadataQuerySpecs([ { sql: safeDbName - ? `SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${safeDbName.toUpperCase()}' ORDER BY SEQUENCE_NAME` + ? `SELECT OWNER AS schema_name, OBJECT_NAME AS sequence_name FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE = 'SEQUENCE' ORDER BY OBJECT_NAME` : `SELECT SEQUENCE_NAME AS sequence_name FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME`, }, + { + // Some Oracle-compatible servers expose ALL_SEQUENCES only partially. + // ALL_OBJECTS is also the catalog used by packages and routines, while + // this keeps ALL_SEQUENCES as a fallback for standard Oracle servers. + sql: safeDbName + ? `SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${safeDbName.toUpperCase()}' ORDER BY SEQUENCE_NAME` + : "", + }, ]); default: return [];