fix(oracle): render raw values as hex

This commit is contained in:
t8y2 2026-07-09 21:14:10 +08:00
parent d25b142cf8
commit 061240390e
2 changed files with 67 additions and 7 deletions

View File

@ -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{}

View File

@ -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{
"": "",