diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 880d48080..f26ea20a7 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -1801,7 +1801,7 @@ func readQuerySessionPage(session *querySession, pageSize int) (queryPageResult, if !session.rows.Next() { return result, session.rows.Err() } - row, err := scanRow(session.rows, len(session.columns)) + row, err := scanRow(session.rows, len(session.columns), session.columnTypes) if err != nil { return queryPageResult{}, err } @@ -1813,7 +1813,7 @@ func readQuerySessionPage(session *querySession, pageSize int) (queryPageResult, return result, nil } if session.rows.Next() { - row, err := scanRow(session.rows, len(session.columns)) + row, err := scanRow(session.rows, len(session.columns), session.columnTypes) if err != nil { return queryPageResult{}, err } @@ -1863,13 +1863,14 @@ func (s *server) executeSelect(sqlText string, maxRows int) (queryResult, error) if err != nil { return queryResult{}, err } - result := queryResult{Columns: columns, ColumnTypes: columnTypeNames(rows), Rows: [][]any{}} + columnTypes := columnTypeNames(rows) + result := queryResult{Columns: columns, ColumnTypes: columnTypes, Rows: [][]any{}} for rows.Next() { if len(result.Rows) >= maxRows { result.Truncated = true break } - values, err := scanRow(rows, len(columns)) + values, err := scanRow(rows, len(columns), columnTypes) if err != nil { return queryResult{}, err } @@ -1878,7 +1879,7 @@ func (s *server) executeSelect(sqlText string, maxRows int) (queryResult, error) return result, rows.Err() } -func scanRow(rows *sql.Rows, columnCount int) ([]any, error) { +func scanRow(rows *sql.Rows, columnCount int, columnTypes []string) ([]any, error) { values := make([]any, columnCount) scanTargets := make([]any, columnCount) for i := range values { @@ -1888,11 +1889,18 @@ func scanRow(rows *sql.Rows, columnCount int) ([]any, error) { return nil, err } for i, value := range values { - values[i] = normalizeValue(value) + values[i] = normalizeValue(value, columnTypeAt(columnTypes, i)) } return values, nil } +func columnTypeAt(columnTypes []string, index int) string { + if index < 0 || index >= len(columnTypes) { + return "" + } + return columnTypes[index] +} + func columnTypeNames(rows *sql.Rows) []string { types, err := rows.ColumnTypes() if err != nil { @@ -2702,11 +2710,15 @@ func quoteIdentifier(value string) string { return `"` + strings.ReplaceAll(value, `"`, `""`) + `"` } -func normalizeValue(value any) any { +func normalizeValue(value any, columnTypeName string) any { switch v := value.(type) { case nil: return nil case []byte: + // Oracle RAW-like columns are binary data; decoding them as text produces mojibake. + if isOracleBinaryColumnType(columnTypeName) { + return bytesToHex(v) + } return string(v) case time.Time: return v.Format(time.RFC3339Nano) @@ -2719,6 +2731,28 @@ func normalizeValue(value any) any { } } +func isOracleBinaryColumnType(columnTypeName string) bool { + normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(columnTypeName), " ", "")) + switch normalized { + case "RAW", "VARRAW", "LONGRAW", "LONGVARRAW", "BLOB", "BFILE", "OCIBLOBLOCATOR", "OCIFILELOCATOR": + return true + default: + return false + } +} + +func bytesToHex(bytes []byte) string { + const digits = "0123456789abcdef" + result := make([]byte, 2+len(bytes)*2) + result[0] = '0' + result[1] = 'x' + for i, b := range bytes { + result[2+i*2] = digits[b>>4] + result[3+i*2] = digits[b&0x0f] + } + return string(result) +} + func emptyIfNil[T any](values []T) []T { if values == nil { return []T{} diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index 8a20b0abe..82f253679 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -129,6 +129,32 @@ func TestGetTableDDLResultMarshalsAsString(t *testing.T) { } } +func TestNormalizeValueFormatsOracleBinaryColumnsAsHex(t *testing.T) { + tests := map[string]string{ + "RAW": "0x000f10ff", + "raw": "0x000f10ff", + "LongRaw": "0x000f10ff", + "LONG RAW": "0x000f10ff", + "LongVarRaw": "0x000f10ff", + "OCIBlobLocator": "0x000f10ff", + } + + for columnType, want := range tests { + if got := normalizeValue([]byte{0x00, 0x0f, 0x10, 0xff}, columnType); got != want { + t.Fatalf("normalizeValue RAW bytes for %q = %#v, want %q", columnType, got, want) + } + } +} + +func TestNormalizeValueKeepsNonBinaryBytesAsText(t *testing.T) { + if got := normalizeValue([]byte("hello"), "VARCHAR2"); got != "hello" { + t.Fatalf("normalizeValue text bytes = %#v, want %q", got, "hello") + } + if got := normalizeValue([]byte("legacy"), ""); got != "legacy" { + t.Fatalf("normalizeValue bytes without metadata = %#v, want %q", got, "legacy") + } +} + func TestNormalizeDDLObjectType(t *testing.T) { tests := map[string]string{ "": "",