feat(oracle): expose synonyms in schema tree

Closes #4655
This commit is contained in:
t8y2
2026-08-12 05:52:08 +00:00
parent ec4f8582e6
commit 4e3a8e4110
4 changed files with 120 additions and 11 deletions
+34 -3
View File
@@ -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
+80 -5
View File
@@ -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"`
@@ -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");
});
@@ -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<DatabaseType>(["oracle", "xugu"]);
@@ -49,7 +50,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
// 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],