mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-11 14:41:21 +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
|
// Value implements driver.Valuer interface
|
||||||
|
// 必须返回 string 而非 []byte:PG simple protocol 下 []byte 参数按 bytea
|
||||||
|
// 编码,写 json 列会触发 SQLSTATE 22P02。
|
||||||
func (c ChannelInfo) Value() (driver.Value, error) {
|
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
|
// Scan implements sql.Scanner interface
|
||||||
func (c *ChannelInfo) Scan(value interface{}) error {
|
func (c *ChannelInfo) Scan(value interface{}) error {
|
||||||
bytesValue, _ := value.([]byte)
|
return common.Unmarshal(jsonScanBytes(value), c)
|
||||||
return common.Unmarshal(bytesValue, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) GetKeys() []string {
|
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 logKeyCol string
|
||||||
var logGroupCol 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() {
|
func initCol() {
|
||||||
// init common column names
|
// init common column names
|
||||||
if common.UsingMainDatabase(common.DatabaseTypePostgreSQL) {
|
if common.UsingMainDatabase(common.DatabaseTypePostgreSQL) {
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import (
|
|||||||
type JSONValue json.RawMessage
|
type JSONValue json.RawMessage
|
||||||
|
|
||||||
// Value 实现 driver.Valuer 接口,用于数据库写入
|
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||||
|
// 必须返回 string 而非 []byte:PG simple protocol 下 []byte 按 bytea 编码,
|
||||||
|
// 写 json 列会触发 SQLSTATE 22P02。
|
||||||
func (j JSONValue) Value() (driver.Value, error) {
|
func (j JSONValue) Value() (driver.Value, error) {
|
||||||
if j == nil {
|
if j == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return []byte(j), nil
|
return string(j), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan 实现 sql.Scanner 接口,兼容不同驱动返回的类型
|
// Scan 实现 sql.Scanner 接口,兼容不同驱动返回的类型
|
||||||
|
|||||||
+15
-4
@@ -87,7 +87,7 @@ type Properties struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Properties) Scan(val interface{}) error {
|
func (m *Properties) Scan(val interface{}) error {
|
||||||
bytesValue, _ := val.([]byte)
|
bytesValue := jsonScanBytes(val)
|
||||||
if len(bytesValue) == 0 {
|
if len(bytesValue) == 0 {
|
||||||
*m = Properties{}
|
*m = Properties{}
|
||||||
return nil
|
return nil
|
||||||
@@ -99,7 +99,13 @@ func (m Properties) Value() (driver.Value, error) {
|
|||||||
if m == (Properties{}) {
|
if m == (Properties{}) {
|
||||||
return nil, nil
|
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 {
|
type TaskPrivateData struct {
|
||||||
@@ -180,7 +186,7 @@ func GenerateTaskID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *TaskPrivateData) Scan(val interface{}) error {
|
func (p *TaskPrivateData) Scan(val interface{}) error {
|
||||||
bytesValue, _ := val.([]byte)
|
bytesValue := jsonScanBytes(val)
|
||||||
if len(bytesValue) == 0 {
|
if len(bytesValue) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -191,7 +197,12 @@ func (p TaskPrivateData) Value() (driver.Value, error) {
|
|||||||
if (p == TaskPrivateData{}) {
|
if (p == TaskPrivateData{}) {
|
||||||
return nil, nil
|
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 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
|
// SyncTaskQueryParams 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
|
||||||
|
|||||||
Reference in New Issue
Block a user