Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require (
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.8.1
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc
github.com/vmihailenco/msgpack/v5 v5.4.1
)

Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
Expand Down
40 changes: 40 additions & 0 deletions internal/dbtest/msgpack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dbtest_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
)

// TestMsgpackRoundTrip covers the reported symptom directly: a msgpack column is
// written as a byte literal, and each dialect spells those differently. Writing
// PostgreSQL's '\x..' form everywhere stored the value as text on the others,
// where it could no longer be decoded.
func TestMsgpackRoundTrip(t *testing.T) {
type Item struct {
Something int `msgpack:"something"`
}

type Model struct {
bun.BaseModel `bun:"table:msgpack_models"`

ID int64 `bun:",pk,autoincrement"`
Encoded Item `bun:",msgpack"`
}

testEachDB(t, func(t *testing.T, dbName string, db *bun.DB) {
ctx := context.Background()
mustResetModel(t, ctx, db, (*Model)(nil))

inserted := &Model{Encoded: Item{Something: 1}}
_, err := db.NewInsert().Model(inserted).Exec(ctx)
require.NoError(t, err)

selected := new(Model)
err = db.NewSelect().Model(selected).Where("id = ?", inserted.ID).Scan(ctx)
require.NoError(t, err)
require.Equal(t, inserted.Encoded, selected.Encoded)
})
}
43 changes: 0 additions & 43 deletions internal/hex.go

This file was deleted.

58 changes: 53 additions & 5 deletions schema/append_value.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package schema

import (
"bytes"
"database/sql/driver"
"fmt"
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"

"github.com/puzpuzpuz/xsync/v3"
Expand Down Expand Up @@ -289,22 +291,68 @@ func addrAppender(fn AppenderFunc) AppenderFunc {
}
}

// maxPooledMsgpackBuffer bounds what the pool keeps alive between queries.
// bytes.Buffer.Reset keeps its backing array, so a writer grown past this by one
// oversized value is dropped instead of retained.
const maxPooledMsgpackBuffer = 64 << 10

var msgpackWriterPool = sync.Pool{
New: func() any { return new(msgpackWriter) },
}

// msgpackWriter stages encoder output and records whether the encoder wrote at
// all. A msgpack.CustomEncoder may return successfully without touching the
// writer, which is SQL NULL, and that is a different outcome from an encoder
// that wrote zero bytes, which is an empty value. Buffer emptiness cannot tell
// the two apart, so the bit is tracked explicitly.
type msgpackWriter struct {
buf bytes.Buffer
written bool
}

func (w *msgpackWriter) Write(b []byte) (int, error) {
w.written = true
return w.buf.Write(b)
}

func (w *msgpackWriter) reset() {
w.buf.Reset()
w.written = false
}

func appendMsgpack(gen QueryGen, b []byte, v reflect.Value) []byte {
hexEnc := internal.NewHexEncoder(b)
// The dialect needs the whole encoded value to wrap it in its own byte
// literal syntax, so the msgpack output is staged rather than streamed
// straight into the query.
w := msgpackWriterPool.Get().(*msgpackWriter)
defer func() {
if w.buf.Cap() <= maxPooledMsgpackBuffer {
w.reset()
msgpackWriterPool.Put(w)
}
}()

enc := msgpack.GetEncoder()
defer msgpack.PutEncoder(enc)

enc.Reset(hexEnc)
enc.Reset(w)
if err := enc.EncodeValue(v); err != nil {
return dialect.AppendError(b, err)
}

if err := hexEnc.Close(); err != nil {
return dialect.AppendError(b, err)
if !w.written {
return dialect.AppendNull(b)
}

// The encoder did write, so this is a value even when it wrote no bytes.
// Bytes() is still nil after a zero-length write, and AppendBytes maps nil
// to NULL, so spell the empty case explicitly.
bs := w.buf.Bytes()
if bs == nil {
bs = []byte{}
}

return hexEnc.Bytes()
return gen.Dialect().AppendBytes(b, bs)
}

func AppendQueryAppender(gen QueryGen, b []byte, app QueryAppender) []byte {
Expand Down
Loading
Loading