diff --git a/internal/dbtest/msgpack_test.go b/internal/dbtest/msgpack_test.go new file mode 100644 index 000000000..08b900456 --- /dev/null +++ b/internal/dbtest/msgpack_test.go @@ -0,0 +1,91 @@ +package dbtest_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/uptrace/bun" +) + +// TestMsgpackTag verifies the behaviour of the `,msgpack` struct tag per +// dialect. The tag encodes values as a PostgreSQL bytea hex literal, so it +// only works on PostgreSQL. On other dialects the value used to be stored +// verbatim as text and silently corrupted, failing on scan with a cryptic +// "msgpack: unexpected code=5c" error (#1219, #519). It must now fail fast at +// insert time with a clear, actionable error instead. +func TestMsgpackTag(t *testing.T) { + type Item struct { + Something int `msgpack:"something"` + } + + type NonPGMsgpackModel struct { + bun.BaseModel `bun:"table:msgpack_models"` + + ID int64 `bun:",pk,autoincrement"` + Name string `bun:",notnull"` + Encoded Item `bun:",msgpack"` + } + + t.Run("non-pg", func(t *testing.T) { + t.Run("sqlite", func(t *testing.T) { + db := sqlite(t) + ctx := context.Background() + mustResetModel(t, ctx, db, (*NonPGMsgpackModel)(nil)) + + model := &NonPGMsgpackModel{Name: "test", Encoded: Item{Something: 1}} + + // Non-PostgreSQL: the insert must fail rather than silently storing a + // corrupted value. Following bun's existing value-append error pattern + // (see appendDriverValue), the readable reason is embedded in the + // generated SQL via dialect.AppendError and the database rejects it. + q := db.NewInsert().Model(model) + require.Contains(t, q.String(), + "msgpack struct tag is only supported by the PostgreSQL dialect") + + _, err := q.Exec(ctx) + require.Error(t, err) + }) + }) + + t.Run("pg", func(t *testing.T) { + testMsgpackTagPostgres(t, pg(t)) + }) + + t.Run("pgx", func(t *testing.T) { + testMsgpackTagPostgres(t, pgx(t)) + }) +} + +func testMsgpackTagPostgres(t *testing.T, db *bun.DB) { + type Item struct { + Something int `msgpack:"something"` + } + + type MsgpackModel struct { + bun.BaseModel `bun:"table:msgpack_models"` + + ID int64 `bun:",pk,autoincrement"` + Name string `bun:",notnull"` + Encoded Item `bun:"type:bytea,msgpack"` + } + + t.Helper() + + t.Run(db.String(), func(t *testing.T) { + ctx := context.Background() + mustResetModel(t, ctx, db, (*MsgpackModel)(nil)) + + model := &MsgpackModel{Name: "test", Encoded: Item{Something: 1}} + + // Regression: msgpack must keep round-tripping on PostgreSQL. + _, err := db.NewInsert().Model(model).Exec(ctx) + require.NoError(t, err) + + got := new(MsgpackModel) + err = db.NewSelect().Model(got).Where("id = ?", model.ID).Scan(ctx) + require.NoError(t, err) + require.Equal(t, model.Encoded, got.Encoded) + }) +} diff --git a/schema/append_value.go b/schema/append_value.go index fb3166daf..50bc5fd6f 100644 --- a/schema/append_value.go +++ b/schema/append_value.go @@ -2,6 +2,7 @@ package schema import ( "database/sql/driver" + "errors" "fmt" "net" "reflect" @@ -289,7 +290,18 @@ func addrAppender(fn AppenderFunc) AppenderFunc { } } +// appendMsgpack encodes v as msgpack and appends it as a PostgreSQL bytea hex +// literal (\x...). The msgpack struct tag is only supported by the PostgreSQL +// dialect: other dialects store the \x literal verbatim as text, so the value +// fails to decode on scan (see issues #1219 and #519). For non-PostgreSQL +// dialects use a custom type (driver.Valuer / sql.Scanner) instead. func appendMsgpack(gen QueryGen, b []byte, v reflect.Value) []byte { + if gen.Dialect().Name() != dialect.PG { + return dialect.AppendError(b, errors.New( + "bun: the msgpack struct tag is only supported by the PostgreSQL dialect; "+ + "for other dialects use a custom type (driver.Valuer / sql.Scanner)")) + } + hexEnc := internal.NewHexEncoder(b) enc := msgpack.GetEncoder() diff --git a/schema/scan.go b/schema/scan.go index 306f55f6f..cd5c7bcbe 100644 --- a/schema/scan.go +++ b/schema/scan.go @@ -331,6 +331,10 @@ func scanScanner(dest reflect.Value, src any) error { return dest.Interface().(sql.Scanner).Scan(src) } +// scanMsgpack decodes a msgpack-encoded value. The msgpack struct tag is only +// supported by the PostgreSQL dialect; on other dialects appendMsgpack rejects +// the value at insert time, so a corrupted value never reaches this scanner +// (see issues #1219 and #519). func scanMsgpack(dest reflect.Value, src any) error { if src == nil { return scanNull(dest)