diff --git a/model/channel.go b/model/channel.go index 397e94289b..705c852b7a 100644 --- a/model/channel.go +++ b/model/channel.go @@ -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 { diff --git a/model/json_column_test.go b/model/json_column_test.go new file mode 100644 index 0000000000..59272629d0 --- /dev/null +++ b/model/json_column_test.go @@ -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) + }) + } +} diff --git a/model/main.go b/model/main.go index 3c10c3d36b..dd2920b0a3 100644 --- a/model/main.go +++ b/model/main.go @@ -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) { diff --git a/model/prefill_group.go b/model/prefill_group.go index cc2e64da99..0d3e3d1ef0 100644 --- a/model/prefill_group.go +++ b/model/prefill_group.go @@ -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 接口,兼容不同驱动返回的类型 diff --git a/model/task.go b/model/task.go index 5263c54811..efbb6c8177 100644 --- a/model/task.go +++ b/model/task.go @@ -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 用于包含所有搜索条件的结构体,可以根据需求添加更多字段