Files
new-api/model/json_column_test.go
T
CaIon 6eb6f35ed2 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.
2026-08-30 21:13:21 +08:00

95 lines
2.6 KiB
Go

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)
})
}
}