mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-13 15:54:34 +00:00
fix(model): return string from JSON column Valuers for pg simple protocol
With PrepareStmt disabled, PostgreSQL queries run over pgx's simple
protocol, which encodes every []byte parameter as a bytea hex literal
('\x...'). driver.Valuer implementations returning []byte from
json.Marshal therefore fail json-column writes with SQLSTATE 22P02
(reported on the channels UPDATE path via ChannelInfo).
Reproduced against a live PostgreSQL 16: []byte Valuer into a json
column fails under simple protocol, string succeeds; []byte into a
text column silently stores the hex literal (no such path exists in
the repo today — audited all Valuers, json.RawMessage fields, and raw
SQL call sites).
- ChannelInfo, Properties, TaskPrivateData, JSONValue Value() now
return string; zero-value nil semantics unchanged. Task.Data
(bare json.RawMessage) is unaffected — database/sql's default
converter already passes it as expected.
- Their Scan() counterparts now accept both []byte and string via a
shared jsonScanBytes helper: SQLite returns string for these columns
once Value() emits string, and the old []byte-only assertions
silently zeroed the field (caught by the model test suite).
- Add regression tests locking both contracts: json-column Valuers
must return string (or nil for zero values), Scanners must accept
[]byte and string.
Verified end-to-end against PostgreSQL 16 with the real model types:
Channel create/update/read-back, Task json fields, PrefillGroup items.
This commit is contained in:
+8
-3
@@ -162,14 +162,19 @@ func ApplyChannelGroupFilter(query *gorm.DB, group string) *gorm.DB {
|
||||
}
|
||||
|
||||
// Value implements driver.Valuer interface
|
||||
// 必须返回 string 而非 []byte:PG simple protocol 下 []byte 参数按 bytea
|
||||
// 编码,写 json 列会触发 SQLSTATE 22P02。
|
||||
func (c ChannelInfo) Value() (driver.Value, error) {
|
||||
return common.Marshal(&c)
|
||||
b, err := common.Marshal(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Scan implements sql.Scanner interface
|
||||
func (c *ChannelInfo) Scan(value interface{}) error {
|
||||
bytesValue, _ := value.([]byte)
|
||||
return common.Unmarshal(bytesValue, c)
|
||||
return common.Unmarshal(jsonScanBytes(value), c)
|
||||
}
|
||||
|
||||
func (channel *Channel) GetKeys() []string {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 保护契约:PostgreSQL 走 simple protocol(PrepareStmt 关闭)时,driver.Valuer
|
||||
// 返回 []byte 会被 pgx 按 bytea 十六进制字面量编码,写入 json 列触发
|
||||
// SQLSTATE 22P02。所有 json 列的 Value() 必须返回 string(或 nil)。
|
||||
func TestJSONColumnValuersReturnString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
valuer driver.Valuer
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ChannelInfo",
|
||||
valuer: ChannelInfo{IsMultiKey: true, MultiKeySize: 2},
|
||||
want: `{"is_multi_key":true,"multi_key_size":2,"multi_key_status_list":null,"multi_key_polling_index":0,"multi_key_mode":""}`,
|
||||
},
|
||||
{
|
||||
name: "Properties",
|
||||
valuer: Properties{Input: "hello"},
|
||||
want: `{"input":"hello"}`,
|
||||
},
|
||||
{
|
||||
name: "TaskPrivateData",
|
||||
valuer: TaskPrivateData{Key: "k"},
|
||||
want: `{"key":"k"}`,
|
||||
},
|
||||
{
|
||||
name: "JSONValue",
|
||||
valuer: JSONValue(`[{"k":"v"}]`),
|
||||
want: `[{"k":"v"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
value, err := testCase.valuer.Value()
|
||||
require.NoError(t, err)
|
||||
str, ok := value.(string)
|
||||
require.True(t, ok, "Value() must return string, got %T", value)
|
||||
assert.JSONEq(t, testCase.want, str)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 空值仍返回 nil,保持列的 NULL 语义。
|
||||
func TestJSONColumnValuersZeroValueIsNil(t *testing.T) {
|
||||
for name, valuer := range map[string]driver.Valuer{
|
||||
"Properties": Properties{},
|
||||
"TaskPrivateData": TaskPrivateData{},
|
||||
"JSONValue": JSONValue(nil),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
value, err := valuer.Value()
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 保护契约:json 列的 Scan 必须同时接受 []byte 与 string——不同驱动/协议
|
||||
// 模式返回类型不同,静默丢弃 string 会把已有数据清零。
|
||||
func TestJSONColumnScannersAcceptStringAndBytes(t *testing.T) {
|
||||
toInput := func(kind string, payload string) interface{} {
|
||||
if kind == "bytes" {
|
||||
return []byte(payload)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
for _, kind := range []string{"bytes", "string"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
var info ChannelInfo
|
||||
require.NoError(t, info.Scan(toInput(kind, `{"is_multi_key":true,"multi_key_size":2}`)))
|
||||
assert.True(t, info.IsMultiKey)
|
||||
assert.Equal(t, 2, info.MultiKeySize)
|
||||
|
||||
var props Properties
|
||||
require.NoError(t, props.Scan(toInput(kind, `{"input":"hello"}`)))
|
||||
assert.Equal(t, "hello", props.Input)
|
||||
|
||||
var private TaskPrivateData
|
||||
require.NoError(t, private.Scan(toInput(kind, `{"key":"k"}`)))
|
||||
assert.Equal(t, "k", private.Key)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,19 @@ var commonFalseVal string
|
||||
var logKeyCol string
|
||||
var logGroupCol string
|
||||
|
||||
// jsonScanBytes 归一化 json 列的驱动返回值:不同驱动/协议模式下同一列可能
|
||||
// 以 []byte 或 string 返回,静默丢弃 string 会导致字段被清零而不报错。
|
||||
func jsonScanBytes(value interface{}) []byte {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return v
|
||||
case string:
|
||||
return []byte(v)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func initCol() {
|
||||
// init common column names
|
||||
if common.UsingMainDatabase(common.DatabaseTypePostgreSQL) {
|
||||
|
||||
@@ -20,11 +20,13 @@ import (
|
||||
type JSONValue json.RawMessage
|
||||
|
||||
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||
// 必须返回 string 而非 []byte:PG simple protocol 下 []byte 按 bytea 编码,
|
||||
// 写 json 列会触发 SQLSTATE 22P02。
|
||||
func (j JSONValue) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(j), nil
|
||||
return string(j), nil
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口,兼容不同驱动返回的类型
|
||||
|
||||
+15
-4
@@ -87,7 +87,7 @@ type Properties struct {
|
||||
}
|
||||
|
||||
func (m *Properties) Scan(val interface{}) error {
|
||||
bytesValue, _ := val.([]byte)
|
||||
bytesValue := jsonScanBytes(val)
|
||||
if len(bytesValue) == 0 {
|
||||
*m = Properties{}
|
||||
return nil
|
||||
@@ -99,7 +99,13 @@ func (m Properties) Value() (driver.Value, error) {
|
||||
if m == (Properties{}) {
|
||||
return nil, nil
|
||||
}
|
||||
return common.Marshal(m)
|
||||
// 必须返回 string 而非 []byte:PG simple protocol 下 []byte 按 bytea 编码,
|
||||
// 写 json 列会触发 SQLSTATE 22P02。
|
||||
b, err := common.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
type TaskPrivateData struct {
|
||||
@@ -180,7 +186,7 @@ func GenerateTaskID() string {
|
||||
}
|
||||
|
||||
func (p *TaskPrivateData) Scan(val interface{}) error {
|
||||
bytesValue, _ := val.([]byte)
|
||||
bytesValue := jsonScanBytes(val)
|
||||
if len(bytesValue) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -191,7 +197,12 @@ func (p TaskPrivateData) Value() (driver.Value, error) {
|
||||
if (p == TaskPrivateData{}) {
|
||||
return nil, nil
|
||||
}
|
||||
return common.Marshal(p)
|
||||
// 同 Properties.Value:string 避免 PG simple protocol 的 bytea 编码。
|
||||
b, err := common.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// SyncTaskQueryParams 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
|
||||
|
||||
Reference in New Issue
Block a user