Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 66 additions & 45 deletions backend/internal/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions backend/internal/mapper/render_test.go
Original file line number Diff line number Diff line change
@@ -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"])
}
}
9 changes: 7 additions & 2 deletions frontend/src/renderer/features/table-viewer/TableDataTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,17 @@ export const TableTabPane: React.FC<TableTabPaneProps> = ({
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);
}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/renderer/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
6 changes: 6 additions & 0 deletions requirement/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,9 @@ add:
- [ ] 触发器变更
创建/修改触发器
启用/禁用触发器

---

fix:
- [x] PostgreSQL JSON/JSONB 字段渲染成 `map[...]` 字符串
- [x] PostgreSQL UUID 字段渲染成字节数组 `[...]`
Loading