Skip to content

Commit 3b02f1d

Browse files
emagda97Yuan325
andauthored
fix: normalize postgres UUIDs to strings (#3806)
## Description Fixes a bug where PostgreSQL UUID columns were being serialized as numerical arrays (e.g., [1, 158, 103, ...]) in JSON output instead of standard hyphenated strings. **Root Cause** The generic results.Values() method in the pgx driver returns UUIDs as [16]byte fixed arrays. Go's standard encoding/json package serializes fixed arrays of bytes as numerical arrays rather than strings. **Solution** Introduced a centralized NormalizeValue helper function in internal/sources/util.go that intercepts values based on their PostgreSQL Object Identifier (OID). If it detects a UUID OID (2950) and a [16]byte type, it converts it into a standard UUID string via the github.com/google/uuid package. **Impact** Consistent string serialization for UUIDs across all Postgres-compatible drivers (postgres, alloydb-postgres, cloud-sql-postgres, yugabytedb). Added unit tests to internal/sources/util_test.go to guarantee reliability. ## PR Checklist - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/mcp-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/mcp-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #3795 Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
1 parent 4a85d75 commit 3b02f1d

6 files changed

Lines changed: 97 additions & 4 deletions

File tree

‎internal/sources/alloydbpg/alloydb_pg.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an
121121
}
122122
row := orderedmap.Row{}
123123
for i, f := range fields {
124-
row.Add(f.Name, v[i])
124+
val := sources.NormalizeValue(v[i], f.DataTypeOID)
125+
row.Add(f.Name, val)
125126
}
126127
out = append(out, row)
127128
}

‎internal/sources/cloudsqlpg/cloud_sql_pg.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an
127127
}
128128
row := orderedmap.Row{}
129129
for i, f := range fields {
130-
row.Add(f.Name, values[i])
130+
val := sources.NormalizeValue(values[i], f.DataTypeOID)
131+
row.Add(f.Name, val)
131132
}
132133
out = append(out, row)
133134
}

‎internal/sources/postgres/postgres.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an
125125
}
126126
row := orderedmap.Row{}
127127
for i, f := range fields {
128-
row.Add(f.Name, values[i])
128+
val := sources.NormalizeValue(values[i], f.DataTypeOID)
129+
row.Add(f.Name, val)
129130
}
130131
out = append(out, row)
131132
}

‎internal/sources/util.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,42 @@ import (
2323
"strings"
2424

2525
"cloud.google.com/go/cloudsqlconn"
26+
"github.com/google/uuid"
2627
"golang.org/x/oauth2/google"
2728
)
2829

30+
// NormalizeValue converts specific database types to friendly representations (e.g. UUID to string).
31+
func NormalizeValue(val any, oid uint32) any {
32+
// 2950 is the standard OID for UUID in PostgreSQL
33+
if oid == 2950 {
34+
if b, ok := val.([16]byte); ok {
35+
return uuid.UUID(b).String()
36+
}
37+
}
38+
// 2951 is the standard OID for _uuid (UUID array) in PostgreSQL
39+
if oid == 2951 {
40+
if arr, ok := val.([][16]byte); ok {
41+
strs := make([]string, len(arr))
42+
for i, b := range arr {
43+
strs[i] = uuid.UUID(b).String()
44+
}
45+
return strs
46+
}
47+
if arr, ok := val.([]any); ok {
48+
strs := make([]any, len(arr))
49+
for i, item := range arr {
50+
if b, ok := item.([16]byte); ok {
51+
strs[i] = uuid.UUID(b).String()
52+
} else {
53+
strs[i] = item
54+
}
55+
}
56+
return strs
57+
}
58+
}
59+
return val
60+
}
61+
2962
// GetCloudSQLDialOpts retrieve dial options with the right ip type and user agent for cloud sql
3063
// databases.
3164
func GetCloudSQLOpts(ipType, userAgent string, useIAM bool) ([]cloudsqlconn.Option, error) {

‎internal/sources/util_test.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package sources
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestNormalizeValue(t *testing.T) {
22+
// Test case 1: UUID OID (2950) with [16]byte
23+
uuidBytes := [16]byte{1, 158, 103, 138, 45, 22, 120, 229, 146, 32, 191, 118, 245, 114, 247, 50}
24+
expectedUUIDStr := "019e678a-2d16-78e5-9220-bf76f572f732"
25+
26+
result := NormalizeValue(uuidBytes, 2950)
27+
if str, ok := result.(string); !ok || str != expectedUUIDStr {
28+
t.Errorf("Expected UUID string %s, got %v", expectedUUIDStr, result)
29+
}
30+
31+
// Test case 2: UUID OID (2950) with other type
32+
result = NormalizeValue("not-bytes", 2950)
33+
if str, ok := result.(string); !ok || str != "not-bytes" {
34+
t.Errorf("Expected original string 'not-bytes', got %v", result)
35+
}
36+
37+
// Test case 3: Other OID
38+
result = NormalizeValue(uuidBytes, 1000)
39+
if bytes, ok := result.([16]byte); !ok || bytes != uuidBytes {
40+
t.Errorf("Expected original [16]byte array, got %v", result)
41+
}
42+
43+
// Test case 4: UUID Array OID (2951) with [][16]byte
44+
uuidArray := [][16]byte{uuidBytes}
45+
result = NormalizeValue(uuidArray, 2951)
46+
if arr, ok := result.([]string); !ok || len(arr) != 1 || arr[0] != expectedUUIDStr {
47+
t.Errorf("Expected []string with %s, got %v", expectedUUIDStr, result)
48+
}
49+
50+
// Test case 5: UUID Array OID (2951) with []any
51+
uuidAnyArray := []any{uuidBytes, "not-a-uuid"}
52+
result = NormalizeValue(uuidAnyArray, 2951)
53+
if arr, ok := result.([]any); !ok || len(arr) != 2 || arr[0] != expectedUUIDStr || arr[1] != "not-a-uuid" {
54+
t.Errorf("Expected []any with normalized UUID, got %v", result)
55+
}
56+
}

‎internal/sources/yugabytedb/yugabytedb.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ func (s *Source) RunSQL(ctx context.Context, statement string, params []any) (an
115115
}
116116
vMap := make(map[string]any)
117117
for i, f := range fields {
118-
vMap[f.Name] = v[i]
118+
val := sources.NormalizeValue(v[i], f.DataTypeOID)
119+
vMap[f.Name] = val
119120
}
120121
out = append(out, vMap)
121122
}

0 commit comments

Comments
 (0)