From f5ccd116ce4d3b63fac9f85ba0f5c5ac193035df Mon Sep 17 00:00:00 2001 From: circle33 Date: Mon, 9 Mar 2026 12:27:54 +0800 Subject: [PATCH] fix: correctly render PostgreSQL JSON and UUID fields --- backend/internal/mapper/mapper.go | 111 +++++++++++------- backend/internal/mapper/render_test.go | 53 +++++++++ .../features/table-viewer/TableDataTab.tsx | 9 +- frontend/src/renderer/utils/format.ts | 10 +- requirement/todo.md | 6 + 5 files changed, 137 insertions(+), 52 deletions(-) create mode 100644 backend/internal/mapper/render_test.go diff --git a/backend/internal/mapper/mapper.go b/backend/internal/mapper/mapper.go index 63f8bb3..9a7f596 100644 --- a/backend/internal/mapper/mapper.go +++ b/backend/internal/mapper/mapper.go @@ -289,56 +289,77 @@ func mapRoutine(r *pb.RoutineDefinition) ast.RoutineDefinition { } } +func toSafeValue(v interface{}) interface{} { + if v == nil { + return nil + } + + switch val := v.(type) { + case int: + return float64(val) + case int32: + return float64(val) + case int64: + return float64(val) + case uint32: + return float64(val) + case uint64: + return float64(val) + case float32: + return float64(val) + case float64: + return val + case *big.Int: + f, _ := new(big.Float).SetInt(val).Float64() + return f + case pgtype.Numeric: + if val.Valid { + f, _ := new(big.Float).SetInt(val.Int).Float64() + if val.Exp != 0 { + f *= math.Pow10(int(val.Exp)) + } + return f + } else { + return nil + } + case pgtype.UUID: + if val.Valid { + return fmt.Sprintf("%x-%x-%x-%x-%x", val.Bytes[0:4], val.Bytes[4:6], val.Bytes[6:8], val.Bytes[8:10], val.Bytes[10:16]) + } + return nil + case [16]byte: + return fmt.Sprintf("%x-%x-%x-%x-%x", val[0:4], val[4:6], val[6:8], val[8:10], val[10:16]) + case time.Time: + return val.Format(time.RFC3339Nano) + case []byte: + return string(val) + case bool: + return val + case string: + return val + case map[string]interface{}: + newMap := make(map[string]interface{}) + for k, v := range val { + newMap[k] = toSafeValue(v) + } + return newMap + case []interface{}: + newSlice := make([]interface{}, len(val)) + for i, v := range val { + newSlice[i] = toSafeValue(v) + } + return newSlice + default: + return fmt.Sprintf("%v", val) + } +} + func RowsToStructs(rows []map[string]interface{}) []*structpb.Struct { res := make([]*structpb.Struct, 0, len(rows)) for _, r := range rows { safeMap := make(map[string]interface{}) for k, v := range r { - if v == nil { - safeMap[k] = nil - continue - } - - switch val := v.(type) { - case int: - safeMap[k] = float64(val) - case int32: - safeMap[k] = float64(val) - case int64: - safeMap[k] = float64(val) - case uint32: - safeMap[k] = float64(val) - case uint64: - safeMap[k] = float64(val) - case float32: - safeMap[k] = float64(val) - case float64: - safeMap[k] = val - case *big.Int: - f, _ := new(big.Float).SetInt(val).Float64() - safeMap[k] = f - case pgtype.Numeric: - if val.Valid { - f, _ := new(big.Float).SetInt(val.Int).Float64() - if val.Exp != 0 { - f *= math.Pow10(int(val.Exp)) - } - safeMap[k] = f - } else { - safeMap[k] = nil - } - case time.Time: - safeMap[k] = val.Format(time.RFC3339Nano) - case []byte: - safeMap[k] = string(val) - case bool: - safeMap[k] = val - case string: - safeMap[k] = val - default: - // Fallback for types like pgtype.Numeric or other custom structs - safeMap[k] = fmt.Sprintf("%v", val) - } + safeMap[k] = toSafeValue(v) } st, err := structpb.NewStruct(safeMap) if err == nil { diff --git a/backend/internal/mapper/render_test.go b/backend/internal/mapper/render_test.go new file mode 100644 index 0000000..b4254b3 --- /dev/null +++ b/backend/internal/mapper/render_test.go @@ -0,0 +1,53 @@ +package mapper + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +func TestRowsToStructs_ComplexTypes(t *testing.T) { + uuidBytes := [16]byte{1, 116, 198, 254, 102, 239, 65, 167, 181, 184, 91, 153, 0, 123, 137, 96} + + rows := []map[string]interface{}{ + { + "id": 1, + "json": map[string]interface{}{"status": "processing"}, + "uuid": pgtype.UUID{Bytes: uuidBytes, Valid: true}, + "raw_uuid": uuidBytes, + "time": time.Date(2026, 3, 9, 12, 0, 0, 0, time.UTC), + }, + } + + structs := RowsToStructs(rows) + if len(structs) != 1 { + t.Fatalf("expected 1 struct, got %d", len(structs)) + } + + data, _ := json.MarshalIndent(structs[0], "", " ") + fmt.Printf("Rendered Result:\n%s\n", string(data)) + + // Verify UUID format + fields := structs[0].GetFields() + uuidStr := fields["uuid"].GetStringValue() + expectedUUID := "0174c6fe-66ef-41a7-b5b8-5b99007b8960" + if uuidStr != expectedUUID { + t.Errorf("expected UUID %s, got %s", expectedUUID, uuidStr) + } + + rawUuidStr := fields["raw_uuid"].GetStringValue() + if rawUuidStr != expectedUUID { + t.Errorf("expected raw UUID %s, got %s", expectedUUID, rawUuidStr) + } + + // Verify JSON is a struct (not a string) + jsonVal := fields["json"].GetStructValue() + if jsonVal == nil { + t.Error("expected json field to be a struct, got nil") + } else if jsonVal.Fields["status"].GetStringValue() != "processing" { + t.Errorf("expected status processing, got %v", jsonVal.Fields["status"]) + } +} diff --git a/frontend/src/renderer/features/table-viewer/TableDataTab.tsx b/frontend/src/renderer/features/table-viewer/TableDataTab.tsx index fb8a6c8..d5b093e 100644 --- a/frontend/src/renderer/features/table-viewer/TableDataTab.tsx +++ b/frontend/src/renderer/features/table-viewer/TableDataTab.tsx @@ -227,12 +227,17 @@ export const TableTabPane: React.FC = ({ const getEditValue = () => { if (!editingCell) return ''; const { value, dataType } = editingCell; - if (dataType?.includes('json') && typeof value === 'object') { + if ( + (dataType?.includes('json') || typeof value === 'object') && + value !== null && + !(value instanceof Date) + ) { return JSON.stringify(value, null, 2); } else if ( dataType?.includes('timestamp') || dataType?.includes('date') || - dataType?.includes('time') + dataType?.includes('time') || + value instanceof Date ) { return formatTimestamp(value); } diff --git a/frontend/src/renderer/utils/format.ts b/frontend/src/renderer/utils/format.ts index b863aa4..0da3395 100644 --- a/frontend/src/renderer/utils/format.ts +++ b/frontend/src/renderer/utils/format.ts @@ -19,14 +19,14 @@ export const formatDisplayValue = (value: any, dataType?: string) => { const type = dataType?.toLowerCase() || ''; - // If it's a JSON type, handle potential string-encoded JSON or direct objects - if (type.includes('json')) { + // If it's an object/array, or a JSON type, handle it + if (type.includes('json') || typeof value === 'object') { try { - // If it's a string that looks like JSON, try to parse it first to avoid extra escaping - const parsed = typeof value === 'string' ? JSON.parse(value) : value; + // If it's a string that looks like JSON (only for json types), try to parse it + const parsed = type.includes('json') && typeof value === 'string' ? JSON.parse(value) : value; return JSON.stringify(parsed, null, 2); } catch (e) { - // If parsing fails (e.g. it's just a regular string in a json column), return as-is + // If parsing fails, return as-is return String(value); } } diff --git a/requirement/todo.md b/requirement/todo.md index c7d48d0..e017e8e 100644 --- a/requirement/todo.md +++ b/requirement/todo.md @@ -413,3 +413,9 @@ add: - [ ] 触发器变更 创建/修改触发器 启用/禁用触发器 + +--- + +fix: + - [x] PostgreSQL JSON/JSONB 字段渲染成 `map[...]` 字符串 + - [x] PostgreSQL UUID 字段渲染成字节数组 `[...]`