From 147e92ff39cb7e5d98c6c6e254cafc918a6ae430 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 3 Aug 2026 15:18:34 +0800 Subject: [PATCH] fix(oracle): preserve quoted object names in DDL Closes #5186 --- agents/drivers/oracle-go/main.go | 56 +++++++--- agents/drivers/oracle-go/main_test.go | 147 +++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 13 deletions(-) diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 0f14a7de6..280676579 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -1956,12 +1956,26 @@ ORDER BY CASE OWNER`, baseSQL, preferredParam, nameColumn, exactParam, typeColumn, nameColumn) } +func oracleObjectNameCandidates(name string) (string, string, bool) { + exact := strings.TrimSpace(name) + uppercase := strings.ToUpper(exact) + return exact, uppercase, exact != uppercase +} + func (s *server) getColumns(schema, table string) ([]columnInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } - table = strings.ToUpper(strings.TrimSpace(table)) + exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(table) + result, err := s.getColumnsByName(schema, exact) + if err != nil || len(result) > 0 || !hasUppercaseFallback { + return result, err + } + return s.getColumnsByName(schema, uppercase) +} + +func (s *server) getColumnsByName(schema, table string) ([]columnInfo, error) { rows, err := s.queryRows(` SELECT c.COLUMN_NAME, c.DATA_TYPE, @@ -2017,7 +2031,15 @@ func (s *server) loadOracleColumnMeta(schema, table string) ([]oracleColumnMeta, if err != nil { return nil, err } - table = strings.ToUpper(strings.TrimSpace(table)) + exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(table) + result, err := s.loadOracleColumnMetaByName(schema, exact) + if err != nil || len(result) > 0 || !hasUppercaseFallback { + return result, err + } + return s.loadOracleColumnMetaByName(schema, uppercase) +} + +func (s *server) loadOracleColumnMetaByName(schema, table string) ([]oracleColumnMeta, error) { rows, err := s.queryRows(` SELECT COLUMN_NAME, DATA_TYPE FROM ALL_TAB_COLUMNS @@ -2255,7 +2277,7 @@ func (s *server) getTableDDL(schema, table, objectType string) (string, error) { if err != nil { return "", err } - objectType, err = s.resolveDDLObjectType(schema, table, objectType) + objectType, table, err = s.resolveDDLObject(schema, table, objectType) if err != nil { return "", err } @@ -2263,7 +2285,7 @@ func (s *server) getTableDDL(schema, table, objectType string) (string, error) { return s.buildViewDDL(schema, table) } var ddl string - err = db.QueryRow("SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, strings.ToUpper(table), schema).Scan(&ddl) + err = db.QueryRow("SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, table, schema).Scan(&ddl) if err == nil && strings.TrimSpace(ddl) != "" { return ddl, nil } @@ -2273,16 +2295,18 @@ func (s *server) getTableDDL(schema, table, objectType string) (string, error) { return "", err } -func (s *server) resolveDDLObjectType(schema, name, requested string) (string, error) { +func (s *server) resolveDDLObject(schema, name, requested string) (string, string, error) { + exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(name) objectType := normalizeDDLObjectType(requested) if objectType != "" { - return objectType, nil + return objectType, exact, nil } db, err := s.requireDB() if err != nil { - return "", err + return "", "", err } - err = db.QueryRow(` + resolve := func(objectName string) error { + return db.QueryRow(` SELECT OBJECT_TYPE FROM ( SELECT OBJECT_TYPE @@ -2292,14 +2316,22 @@ FROM ( AND OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 ELSE 2 END ) -WHERE ROWNUM = 1`, schema, strings.ToUpper(name)).Scan(&objectType) +WHERE ROWNUM = 1`, schema, objectName).Scan(&objectType) + } + err = resolve(exact) + if errors.Is(err, sql.ErrNoRows) && hasUppercaseFallback { + err = resolve(uppercase) + if err == nil { + exact = uppercase + } + } if errors.Is(err, sql.ErrNoRows) { - return "", fmt.Errorf("object not found: %s.%s", schema, name) + return "", "", fmt.Errorf("object not found: %s.%s", schema, name) } if err != nil { - return "", err + return "", "", err } - return normalizeDDLObjectType(objectType), nil + return normalizeDDLObjectType(objectType), exact, nil } func normalizeDDLObjectType(value string) string { diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index dffbf4fc9..9c010c3c3 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -290,6 +290,146 @@ func TestNormalizeDDLObjectType(t *testing.T) { } } +func TestGetTableDDLPreservesQuotedObjectName(t *testing.T) { + const schema = "ZTZS_ERP2" + const table = "ZGJ_FlowSealTemplate" + const ddl = `CREATE TABLE "ZTZS_ERP2"."ZGJ_FlowSealTemplate" ("FlowId" NUMBER)` + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "FROM ALL_OBJECTS", + args: []driver.Value{schema, table}, + rows: [][]driver.Value{{"TABLE"}}, + }, + { + queryContains: "DBMS_METADATA.GET_DDL", + args: []driver.Value{"TABLE", table, schema}, + rows: [][]driver.Value{{ddl}}, + }, + }) + s := newServer() + s.db = db + + got, err := s.getTableDDL(schema, table, "") + if err != nil { + t.Fatal(err) + } + if got != ddl { + t.Fatalf("getTableDDL() = %q, want %q", got, ddl) + } + if scripted.next != len(scripted.steps) { + t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) + } +} + +func TestGetTableDDLUppercaseObjectDoesNotAddFallbackQuery(t *testing.T) { + const schema = "ZTZS_ERP2" + const table = "ORDERS" + const ddl = `CREATE TABLE "ZTZS_ERP2"."ORDERS" ("ID" NUMBER)` + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "FROM ALL_OBJECTS", + args: []driver.Value{schema, table}, + rows: [][]driver.Value{{"TABLE"}}, + }, + { + queryContains: "DBMS_METADATA.GET_DDL", + args: []driver.Value{"TABLE", table, schema}, + rows: [][]driver.Value{{ddl}}, + }, + }) + s := newServer() + s.db = db + + got, err := s.getTableDDL(schema, table, "") + if err != nil { + t.Fatal(err) + } + if got != ddl { + t.Fatalf("getTableDDL() = %q, want %q", got, ddl) + } + if scripted.next != 2 { + t.Fatalf("uppercase object should use two queries, got %d", scripted.next) + } +} + +func TestGetTableDDLFallsBackToUppercaseAfterExactMiss(t *testing.T) { + const schema = "ZTZS_ERP2" + const ddl = `CREATE TABLE "ZTZS_ERP2"."ORDERS" ("ID" NUMBER)` + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "FROM ALL_OBJECTS", + args: []driver.Value{schema, "orders"}, + rows: nil, + }, + { + queryContains: "FROM ALL_OBJECTS", + args: []driver.Value{schema, "ORDERS"}, + rows: [][]driver.Value{{"TABLE"}}, + }, + { + queryContains: "DBMS_METADATA.GET_DDL", + args: []driver.Value{"TABLE", "ORDERS", schema}, + rows: [][]driver.Value{{ddl}}, + }, + }) + s := newServer() + s.db = db + + got, err := s.getTableDDL(schema, "orders", "") + if err != nil { + t.Fatal(err) + } + if got != ddl { + t.Fatalf("getTableDDL() = %q, want %q", got, ddl) + } + if scripted.next != len(scripted.steps) { + t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) + } +} + +func TestGetTableDDLFallbackPreservesQuotedColumnNames(t *testing.T) { + const schema = "ZTZS_ERP2" + const table = "ZGJ_FlowSealTemplate" + db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ + { + queryContains: "FROM ALL_OBJECTS", + args: []driver.Value{schema, table}, + rows: [][]driver.Value{{"TABLE"}}, + }, + { + queryContains: "DBMS_METADATA.GET_DDL", + args: []driver.Value{"TABLE", table, schema}, + err: errors.New("ORA-31603: object not found"), + }, + { + queryContains: "FROM ALL_TAB_COLUMNS", + args: []driver.Value{schema, table}, + columns: []string{ + "COLUMN_NAME", "DATA_TYPE", "NULLABLE", "DATA_DEFAULT", "IS_PRIMARY_KEY", + "COMMENTS", "DATA_PRECISION", "DATA_SCALE", "CHAR_LENGTH", + }, + rows: [][]driver.Value{{"FlowId", "VARCHAR2", "N", nil, int64(1), nil, nil, nil, int64(50)}}, + }, + }) + s := newServer() + s.db = db + + got, err := s.getTableDDL(schema, table, "") + if err != nil { + t.Fatal(err) + } + want := `CREATE TABLE "ZTZS_ERP2"."ZGJ_FlowSealTemplate" ( + "FlowId" VARCHAR2(50) NOT NULL, + PRIMARY KEY ("FlowId") +)` + if got != want { + t.Fatalf("getTableDDL() = %q, want %q", got, want) + } + if scripted.next != len(scripted.steps) { + t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) + } +} + func TestIsQuerySQLSkipsLeadingComments(t *testing.T) { tests := []string{ "-- 测试\nSELECT * FROM (SELECT * FROM \"DBX_TEST\".\"ORDERS_10K\") WHERE ROWNUM <= 100", @@ -1482,6 +1622,7 @@ func contains(values []string, target string) bool { type oracleViewSourceQueryStep struct { queryContains string args []driver.Value + columns []string rows [][]driver.Value err error } @@ -1534,7 +1675,11 @@ func (c *oracleViewSourceConn) QueryContext( if step.err != nil { return nil, step.err } - return &oracleViewSourceRows{columns: []string{"SOURCE"}, values: step.rows}, nil + columns := step.columns + if len(columns) == 0 { + columns = []string{"SOURCE"} + } + return &oracleViewSourceRows{columns: columns, values: step.rows}, nil } type oracleViewSourceRows struct {