Skip to content
Open
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
16 changes: 16 additions & 0 deletions contrib/drivers/pgsql/pgsql_convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie
var fieldValueKind = reflect.TypeOf(fieldValue).Kind()

if fieldValueKind == reflect.Slice {
// For bytea type, pass []byte directly without any conversion.
if _, ok := fieldValue.([]byte); ok && gstr.Contains(fieldType, "bytea") {
return d.Core.ConvertValueForField(ctx, fieldType, fieldValue)
}
// For pgsql, json or jsonb require '[]'
if !gstr.Contains(fieldType, "json") {
fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue),
Expand Down Expand Up @@ -62,6 +66,7 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie
// | _varchar, _text | []string |
// | _char, _bpchar | []string |
// | _numeric, _decimal, _money | []float64 |
// | bytea | []byte |
// | _bytea | [][]byte |
// | _uuid | []uuid.UUID |
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) {
Expand Down Expand Up @@ -107,6 +112,9 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f
case "_numeric", "_decimal", "_money":
return gdb.LocalTypeFloat64Slice, nil

case "bytea":
return gdb.LocalTypeBytes, nil

case "_bytea":
return gdb.LocalTypeBytesSlice, nil

Expand Down Expand Up @@ -141,6 +149,7 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f
// | _numeric | numeric[] | pq.Float64Array | []float64 |
// | _decimal | decimal[] | pq.Float64Array | []float64 |
// | _money | money[] | pq.Float64Array | []float64 |
// | bytea | bytea | - | []byte |
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
//
Expand All @@ -154,6 +163,13 @@ func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fie
// Basic types are mostly handled by Core layer, only handle array types here
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this function only handles array types, but the newly-added case "bytea" is a basic (non-array) type. Please update the comment to reflect the current behavior so future changes don’t accidentally regress bytea handling.

Suggested change
// Basic types are mostly handled by Core layer, only handle array types here
// Basic types are mostly handled by Core layer; handle array types and special-case bytea here.

Copilot uses AI. Check for mistakes.
switch typeName {

// []byte
case "bytea":
if v, ok := fieldValue.([]byte); ok {
return v, nil
}
return fieldValue, nil
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ConvertValueForLocal, the new bytea branch returns fieldValue unchanged when it’s not already a []byte. For consistency with the bytea -> []byte mapping (and to keep all conversions centralized), it may be clearer to delegate to the Core conversion or explicitly convert non-[]byte inputs to []byte instead of returning them as-is.

Suggested change
return fieldValue, nil
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)

Copilot uses AI. Check for mistakes.

// []int32
case "_int2", "_int4":
var result pq.Int32Array
Expand Down
28 changes: 28 additions & 0 deletions contrib/drivers/pgsql/pgsql_z_unit_convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ func Test_CheckLocalTypeForField(t *testing.T) {
t.Assert(localType, gdb.LocalTypeFloat64Slice)
})

gtest.C(t, func(t *gtest.T) {
// Test bytea type
localType, err := driver.CheckLocalTypeForField(ctx, "bytea", nil)
t.AssertNil(err)
t.Assert(localType, gdb.LocalTypeBytes)
})

gtest.C(t, func(t *gtest.T) {
// Test bytea array type
localType, err := driver.CheckLocalTypeForField(ctx, "_bytea", nil)
Expand Down Expand Up @@ -362,6 +369,17 @@ func Test_ConvertValueForLocal(t *testing.T) {
_, err := driver.ConvertValueForLocal(ctx, "_bytea", "invalid")
t.AssertNE(err, nil)
})

gtest.C(t, func(t *gtest.T) {
// Test bytea conversion - should preserve []byte as-is
input := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x5D, 0x5B}
result, err := driver.ConvertValueForLocal(ctx, "bytea", input)
t.AssertNil(err)
resultBytes, ok := result.([]byte)
t.Assert(ok, true)
t.Assert(len(resultBytes), len(input))
t.Assert(resultBytes, input)
})
}

// Test_ConvertValueForField tests the ConvertValueForField method
Expand Down Expand Up @@ -406,4 +424,14 @@ func Test_ConvertValueForField(t *testing.T) {
t.AssertNil(err)
t.Assert(result, `["a","b"]`)
})

gtest.C(t, func(t *gtest.T) {
// Test []byte value for bytea type (should preserve raw bytes, not do []->{} replacement)
input := []byte{0xDE, 0xAD, 0x5B, 0x5D, 0xBE, 0xEF}
result, err := driver.ConvertValueForField(ctx, "bytea", input)
t.AssertNil(err)
resultBytes, ok := result.([]byte)
t.Assert(ok, true)
t.Assert(resultBytes, input)
})
}
87 changes: 87 additions & 0 deletions contrib/drivers/pgsql/pgsql_z_unit_issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,93 @@ func Test_Issue4500(t *testing.T) {
})
}

// https://github.com/gogf/gf/issues/4677
// record.Get().Bytes() corrupts bytea data on retrieval from PostgreSQL.
func Test_Issue4677(t *testing.T) {
table := fmt.Sprintf(`%s_%d`, TablePrefix+"issue4677", gtime.TimestampNano())
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id bigserial PRIMARY KEY,
bin_data bytea
);`, table,
)); err != nil {
gtest.Fatal(err)
}
defer dropTable(table)

gtest.C(t, func(t *gtest.T) {
// Test 1: Binary data with various byte values including 0x00, 0x5D(']'), 0x5B('[')
originalBytes := []byte{
0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01, 0x5B, 0x5D,
0xFF, 0x7B, 0x7D, 0x80, 0xCA, 0xFE, 0xBA, 0xBE,
}

_, err := db.Model(table).Data(g.Map{
"bin_data": originalBytes,
}).Insert()
t.AssertNil(err)

record, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)

retrievedBytes := record["bin_data"].Bytes()
t.Assert(len(retrievedBytes), len(originalBytes))
t.Assert(retrievedBytes, originalBytes)
})

gtest.C(t, func(t *gtest.T) {
// Test 2: Larger binary data (simulating gob/protobuf encoded payload)
largeBytes := make([]byte, 1024)
for i := range largeBytes {
largeBytes[i] = byte(i % 256)
}

_, err := db.Model(table).Data(g.Map{
"bin_data": largeBytes,
}).Insert()
t.AssertNil(err)

record, err := db.Model(table).OrderDesc("id").One()
t.AssertNil(err)

retrievedBytes := record["bin_data"].Bytes()
t.Assert(len(retrievedBytes), len(largeBytes))
t.Assert(retrievedBytes, largeBytes)
})
}

// https://github.com/gogf/gf/issues/4231
// ConvertValueForField corrupts bytea data containing 0x5D on write.
func Test_Issue4231(t *testing.T) {
table := fmt.Sprintf(`%s_%d`, TablePrefix+"issue4231", gtime.TimestampNano())
if _, err := db.Exec(ctx, fmt.Sprintf(`
CREATE TABLE %s (
id bigserial PRIMARY KEY,
bin_data bytea
);`, table,
)); err != nil {
gtest.Fatal(err)
}
defer dropTable(table)

gtest.C(t, func(t *gtest.T) {
// Bytes containing 0x5D (ASCII ']') which was being converted to 0x7D ('}')
originalBytes := []byte{0x01, 0x5D, 0x02, 0x5B, 0x03}

_, err := db.Model(table).Data(g.Map{
"bin_data": originalBytes,
}).Insert()
t.AssertNil(err)

record, err := db.Model(table).Where("id", 1).One()
t.AssertNil(err)

retrievedBytes := record["bin_data"].Bytes()
t.Assert(len(retrievedBytes), len(originalBytes))
t.Assert(retrievedBytes, originalBytes)
})
}

// https://github.com/gogf/gf/issues/4595
// FieldsPrefix silently drops fields when using table alias before LeftJoin.
func Test_Issue4595(t *testing.T) {
Expand Down
Loading