diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 0eb5c8e871..0e775c4364 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -138,6 +138,12 @@ SELECT o.OBJECT_NAME, FROM ALL_OBJECTS o WHERE o.OWNER = :2 AND o.OBJECT_TYPE IN ('VIEW', 'PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY') +UNION ALL +SELECT s.SYNONYM_NAME AS OBJECT_NAME, + 'SYNONYM' AS OBJECT_TYPE, + CAST(NULL AS VARCHAR2(4000)) AS COMMENTS +FROM ALL_SYNONYMS s +WHERE s.OWNER = :3 )` const oracleListObjectsSessionUserBaseSQL = ` SELECT OBJECT_NAME, OBJECT_TYPE, COMMENTS @@ -153,14 +159,20 @@ SELECT o.OBJECT_NAME, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_OBJECTS o WHERE o.OBJECT_TYPE IN ('VIEW', 'PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY') +UNION ALL +SELECT s.SYNONYM_NAME AS OBJECT_NAME, + 'SYNONYM' AS OBJECT_TYPE, + CAST(NULL AS VARCHAR2(4000)) AS COMMENTS +FROM USER_SYNONYMS s )` const oracleListObjectsOrderSQL = `ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'PROCEDURE' THEN 2 WHEN 'FUNCTION' THEN 3 - WHEN 'PACKAGE' THEN 4 - ELSE 5 + WHEN 'SYNONYM' THEN 4 + WHEN 'PACKAGE' THEN 5 + ELSE 6 END, OBJECT_NAME` const oracleListObjectsSQL = oracleListObjectsBaseSQL + "\n" + oracleListObjectsOrderSQL const oracleListTriggersSQL = ` @@ -1511,7 +1523,7 @@ func oracleListObjectsQuery(schema string, constraints metadataListConstraints) "OBJECT_NAME, OBJECT_TYPE, COMMENTS", "OBJECT_TYPE", oracleListObjectsOrderSQL, - []any{schema, schema}, + []any{schema, schema, schema}, constraints, ) } @@ -2597,6 +2609,9 @@ func (s *server) getObjectSource(schema, name, objectType string) (map[string]an } return map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": source}, nil } + if upperType == "SYNONYM" { + return s.getMetadataObjectSource(schema, name, upperType) + } // Unquoted Oracle identifiers are stored uppercase; quoted mixed-case names must stay exact. // Try caller-provided identity first, then uppercase fallback (same pattern as column metadata). @@ -2612,6 +2627,22 @@ func (s *server) getObjectSource(schema, name, objectType string) (map[string]an return map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": ""}, nil } +func (s *server) getMetadataObjectSource(schema, name, objectType string) (map[string]any, error) { + db, err := s.requireDB() + if err != nil { + return nil, err + } + var lastErr error + for _, candidate := range oracleObjectIdentityNameCandidates(name) { + var source string + lastErr = db.QueryRow("SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, candidate, schema).Scan(&source) + if lastErr == nil { + return map[string]any{"name": candidate, "object_type": objectType, "schema": schema, "source": source}, nil + } + } + return nil, lastErr +} + func (s *server) loadObjectSourceText(schema, name, objectType string) (string, bool, error) { rows, err := s.queryRows(` SELECT TEXT diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index 0d6115de8d..f6155b315b 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -1600,6 +1600,9 @@ func TestListObjectsSQLUsesSplitDictionaryQuery(t *testing.T) { if !strings.Contains(sqlText, "'PACKAGE BODY'") || !strings.Contains(sqlText, "PACKAGE_BODY") { t.Fatalf("object listing should include package bodies with normalized type, got: %s", oracleListObjectsSQL) } + if !strings.Contains(sqlText, "ALL_SYNONYMS") || !strings.Contains(sqlText, "'SYNONYM' AS OBJECT_TYPE") { + t.Fatalf("object listing should include schema synonyms, got: %s", oracleListObjectsSQL) + } } func TestListObjectsQueryAppliesMetadataConstraints(t *testing.T) { @@ -1610,19 +1613,19 @@ func TestListObjectsQueryAppliesMetadataConstraints(t *testing.T) { }) sqlText := strings.ToUpper(query.SQL) - if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :3 ESCAPE '\\'") { + if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :4 ESCAPE '\\'") { t.Fatalf("object listing should push filter predicate, got: %s", query.SQL) } - if !strings.Contains(sqlText, "OBJECT_TYPE IN (:4,:5)") { + if !strings.Contains(sqlText, "OBJECT_TYPE IN (:5,:6)") { t.Fatalf("object listing should push object type predicate, got: %s", query.SQL) } - if !strings.Contains(sqlText, "ROWNUM <= :6") || !strings.Contains(sqlText, "DBX_RN > :7") { + if !strings.Contains(sqlText, "ROWNUM <= :7") || !strings.Contains(sqlText, "DBX_RN > :8") { t.Fatalf("object listing should use rownum pagination, got: %s", query.SQL) } - if len(query.Args) != 7 { + if len(query.Args) != 8 { t.Fatalf("unexpected args: %#v", query.Args) } - if query.Args[2] != "%P%K%G%\\%%" || query.Args[3] != "FUNCTION" || query.Args[4] != "PACKAGE" || query.Args[5] != 25 || query.Args[6] != 0 { + if query.Args[0] != "APP" || query.Args[1] != "APP" || query.Args[2] != "APP" || query.Args[3] != "%P%K%G%\\%%" || query.Args[4] != "FUNCTION" || query.Args[5] != "PACKAGE" || query.Args[6] != 25 || query.Args[7] != 0 { t.Fatalf("object constraints args were not normalized: %#v", query.Args) } } @@ -1641,6 +1644,9 @@ func TestListSessionUserObjectsQueryUsesUserDictionary(t *testing.T) { if strings.Contains(sqlText, "ALL_TABLES") || strings.Contains(sqlText, "ALL_OBJECTS") { t.Fatalf("session-user object listing should avoid ALL_* dictionaries, got: %s", query.SQL) } + if !strings.Contains(sqlText, "USER_SYNONYMS") { + t.Fatalf("session-user object listing should include owned synonyms, got: %s", query.SQL) + } if strings.Contains(sqlText, "OWNER =") { t.Fatalf("session-user object listing should not add owner predicates, got: %s", query.SQL) } @@ -1661,6 +1667,26 @@ func TestListSessionUserObjectsQueryUsesUserDictionary(t *testing.T) { } } +func TestListObjectsQuerySupportsSynonymOnlyFiltering(t *testing.T) { + query := oracleListObjectsQuery("AP", metadataListConstraints{ + Filter: "spec", + Limit: 10, + Offset: 20, + ObjectTypes: []string{"synonym"}, + }) + sqlText := strings.ToUpper(query.SQL) + + if !strings.Contains(sqlText, "FROM ALL_SYNONYMS") || !strings.Contains(sqlText, "S.OWNER = :3") { + t.Fatalf("cross-schema synonym listing should use the selected owner, got: %s", query.SQL) + } + if !strings.Contains(sqlText, "OBJECT_TYPE IN (:5)") { + t.Fatalf("synonym filtering should be pushed into the constrained query, got: %s", query.SQL) + } + if len(query.Args) != 7 || query.Args[2] != "AP" || query.Args[3] != "%S%P%E%C%" || query.Args[4] != "SYNONYM" || query.Args[5] != 30 || query.Args[6] != 20 { + t.Fatalf("unexpected synonym query args: %#v", query.Args) + } +} + func TestOracleListTriggersSQLLoadsSourceWithoutLongColumns(t *testing.T) { sqlText := strings.ToUpper(oracleListTriggersSQL) @@ -2022,6 +2048,55 @@ func TestGetObjectSourceUsesOriginalViewNameWithDBMSMetadata(t *testing.T) { } } +func TestGetObjectSourceUsesDBMSMetadataForSynonym(t *testing.T) { + const ddl = `CREATE OR REPLACE EDITIONABLE SYNONYM "AP"."S_SPECS" FOR "ADM"."S_SPECS"` + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "DBMS_METADATA.GET_DDL(:1, :2, :3)", + args: []driver.Value{"SYNONYM", "S_SPECS", "AP"}, + rows: [][]driver.Value{{ddl}}, + }, + }) + s := newServer() + s.db = db + + result, err := s.getObjectSource("AP", "S_SPECS", "SYNONYM") + if err != nil { + t.Fatal(err) + } + if result["source"] != ddl || result["object_type"] != "SYNONYM" { + t.Fatalf("unexpected synonym source: %#v", result) + } + if scripted.next != len(scripted.steps) { + t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) + } +} + +func TestGetObjectSourcePreservesQuotedSynonymName(t *testing.T) { + const synonym = "MixedSynonym" + const ddl = `CREATE OR REPLACE EDITIONABLE SYNONYM "AP"."MixedSynonym" FOR "ADM"."S_SPECS"` + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "DBMS_METADATA.GET_DDL(:1, :2, :3)", + args: []driver.Value{"SYNONYM", synonym, "AP"}, + rows: [][]driver.Value{{ddl}}, + }, + }) + s := newServer() + s.db = db + + result, err := s.getObjectSource("AP", synonym, "SYNONYM") + if err != nil { + t.Fatal(err) + } + if result["name"] != synonym || result["source"] != ddl { + t.Fatalf("quoted synonym identity was not preserved: %#v", result) + } + if scripted.next != len(scripted.steps) { + t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) + } +} + func TestGetObjectSourceFallsBackToAllViewsWithOriginalName(t *testing.T) { const viewName = "vEnginWJZ" const source = `SELECT source_id FROM "ZTZS_ERP2"."SOURCE_TABLE"` diff --git a/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts index 83e2564a53..42857af483 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts @@ -6,8 +6,10 @@ describe("databaseObjectCapabilities", () => { expect(sidebarObjectKindsForDatabase("dameng")).toEqual(expect.arrayContaining(["MATERIALIZED_VIEW", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"])); }); - it("exposes synonyms for Xugu only", () => { + it("exposes synonyms only for database paths with synonym metadata", () => { + expect(sidebarObjectKindsForDatabase("oracle")).toContain("SYNONYM"); expect(sidebarObjectKindsForDatabase("xugu")).toContain("SYNONYM"); + expect(sidebarObjectKindsForDatabase("oceanbase-oracle")).not.toContain("SYNONYM"); expect(sidebarObjectKindsForDatabase("postgres")).not.toContain("SYNONYM"); }); diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts index f22e0179cb..c63928ac46 100644 --- a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts @@ -29,7 +29,8 @@ const POSTGRES_NO_TYPE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIA const KINGBASE_VASTBASE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TYPE"]; const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION"]; -const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]; +const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SYNONYM", "PACKAGE", "PACKAGE_BODY"]; +const OCEANBASE_ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]; const DAMENG_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"]; const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"]; const PACKAGE_MEMBER_EXPANSION_DATABASES = new Set(["oracle", "xugu"]); @@ -49,7 +50,7 @@ const DATABASE_TYPE_OBJECTS = new Map([ // oracle ["oracle", ORACLE_OBJECTS], ["dameng", DAMENG_OBJECTS], - ["oceanbase-oracle", ORACLE_OBJECTS], + ["oceanbase-oracle", OCEANBASE_ORACLE_OBJECTS], ["xugu", XUGU_OBJECTS], // table and view ["sqlite", TABLE_VIEW_OBJECTS],