pkg/models/postgres_v3_cell.go
Rama main · réplica de lectura del repositorio original.
// postgres_v3_cell.go — logical-value representation for PG row cells.
//
// PRINCIPLE: wire format is an encoding concern, not a capture
// concern. A cell stores the LOGICAL value a PG server would have
// returned — int64 246, time.Time 2026-04-24T14:25:37Z, string
// "priority-i23-333b" — NOT the format-coupled wire bytes. At emit
// time, the v3 replayer looks up the column's TypeOID and the live
// client's requested format, then encodes via the codec registry
// (see integrations/pkg/postgres/v3/codec).
//
// Record and replay decouple: a query captured in one format can
// satisfy a live request in the other because format is derived at
// emit time, not stored in the mock.
//
// Encoding (on-disk YAML): cells serialize as their native Go
// value. Integer cells are plain YAML integers; string cells are
// plain strings; timestamps are ISO 8601; byte slices use yaml.v3's
// native !!binary tag. The result is diffable, reviewable, and
// hand-editable.
//
// JSON is a secondary path used by syncMock / MockOutgoing to ferry
// recorded values through encoding/json. PostgresV3Cell deliberately
// does NOT implement MarshalJSON / UnmarshalJSON — encoding/json's
// reflection emits the struct as `{"Value": ...}` and consumers on
// the receive side decode with the same struct shape. The native-
// scalar guarantee above applies only to the on-disk YAML form.
//
// NULL is cell.Value == nil. An empty-string value ("") is distinct
// from NULL — both round-trip correctly.
package models
import (
"bytes"
"encoding/base64"
"encoding/gob"
"fmt"
"math/big"
"net"
"net/netip"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/jackc/pgx/v5/pgtype"
yaml "gopkg.in/yaml.v3"
)
// PostgresV3Cell is ONE column in ONE row of a recorded SELECT
// response (or ONE parameter in a Bind message). The Value field
// holds the canonical Go representation for the column's type per
// its TypeOID — see integrations/pkg/postgres/v3/codec for the
// per-OID contract.
//
// NULL is cell.Value == nil; prefer the direct `== nil` check over
// the IsNull helper in hot paths.
type PostgresV3Cell struct {
// Value is the logical column value. Go type is dictated by the
// column's TypeOID, matching what pgtype hands back when the
// scan target is *any:
//
// int2 / int4 / int8 → int16 / int32 / int64
// oid / xid / cid → uint32 (pg_catalog metadata)
// xid8 → uint64 (PG ≥ 13)
// float4 / float8 → float32 / float64
// numeric / decimal → pgtype.Numeric (arbitrary
// precision; struct fields go
// through gob-friendly paths)
// bool → bool
// text / varchar / name /
// bpchar / char → string
// timestamp / timestamptz → time.Time
// date / time / timetz → time.Time (time-of-day zero on
// date; pgtype.Time fallback on
// some pgx versions)
// bytea / xml → []byte
// uuid → canonical 8-4-4-4-12 string
// text[] / int4[] / composite-row / multi-dim arrays
// → []interface{} of per-element
// logical type
// json / jsonb → map[string]interface{} for
// top-level objects, []interface{}
// for top-level arrays. Nested
// values use the same recursive
// cell encoding.
// yaml-reloaded nested ints → int (yaml.v3 produces native
// Go int when destination is `any`,
// which is the recursive decode
// path inside slice / jsonb cells)
// unknown OIDs → PostgresV3CellRaw (format-tagged
// bytes, carried through verbatim;
// codec on integrations side duck-
// types on RawBytesAndFormat to
// re-emit without transcoding)
//
// Width caveat: yaml.v3's resolver decodes every !!int back to
// int64, so int16 / int32 cells round-trip through the gob path
// (sidecar → agent stream) with their source Go width but widen
// to int64 through the YAML path (mocks.yaml on disk). That's
// intentional — the codec on the integrations side encodes onto
// the wire using the column OID, not the Go width, so the bytes
// emitted are correct in both cases. Downstream code that type-
// switches on Cell.Value should accept int16/int32/int64 for "an
// integer column" or use a helper that widens.
Value any
}
// legacyNullSentinel is the printable string that pre-PostgresV3Cell
// recordings wrote into [][]string Rows to denote SQL NULL. New
// recordings never emit this string (NULL goes through native YAML
// null / Cell.IsNull); UnmarshalYAML translates it back to nil so
// legacy mocks keep distinguishing NULL from empty-string.
const legacyNullSentinel = "~~KEPLOY_PG_NULL~~"
// NullCell returns a cell representing SQL NULL.
func NullCell() PostgresV3Cell {
return PostgresV3Cell{Value: nil}
}
// NewValueCell wraps an already-decoded logical Go value.
//
// A typed-nil pointer (e.g. (*PostgresV3CellRaw)(nil) handed in by a
// nil-coalesced raw extractor) is collapsed to plain nil so the
// resulting cell behaves consistently across IsNull(), MarshalYAML,
// and GobEncode — without this, the cell would test as not-NULL via
// IsNull but serialize as NULL through gob, which silently breaks
// NULL-vs-empty diffs at replay time.
func NewValueCell(v any) PostgresV3Cell {
if isTypedNilPointer(v) {
v = nil
}
return PostgresV3Cell{Value: v}
}
// IsNull reports whether the cell is SQL NULL. Readable over
// `cell.Value == nil` when context matters. Also returns true for a
// typed-nil pointer (e.g. (*PostgresV3CellRaw)(nil)) so the predicate
// matches the on-disk encoding GobEncode picks for the same shape.
func (c PostgresV3Cell) IsNull() bool {
if c.Value == nil {
return true
}
return isTypedNilPointer(c.Value)
}
// isTypedNilPointer reports whether v is a typed-nil pointer, i.e.
// v != nil at the interface level but the underlying pointer is nil.
// We restrict the check to the one pointer type that GobEncode
// special-cases (*PostgresV3CellRaw). Any other typed-nil arriving
// here is a programmer error and is left alone so it surfaces in
// the GobEncode default branch with a clear "unsupported Value
// type" message.
func isTypedNilPointer(v any) bool {
if raw, ok := v.(*PostgresV3CellRaw); ok && raw == nil {
return true
}
return false
}
// MarshalYAML emits the cell's logical value directly. A row is a
// YAML sequence of cells and each cell is just its value on disk.
// NULL → YAML null; []byte → !!binary base64 (we build the node
// explicitly because yaml.v3's any-wrapped []byte goes through the
// generic slice encoder and produces a sequence-of-ints instead of
// the binary tag).
//
// String values are screened through StringNeedsDoubleQuoted; when
// that predicate matches, MarshalYAML returns an explicit yaml.Node
// with yaml.DoubleQuotedStyle instead of the raw string. The escape
// keeps the emitter out of the literal-block-scalar branch — yaml.v3
// has a long-standing bug where strings starting with "\n\t" or
// containing embedded tabs emit as `|4-` block scalars whose content
// tab disrupts indent detection when the scalar lives inside a
// sequence (the shape PostgresV3Cells produces), so the same mock
// file the recorder just wrote back fails to load on replay. The
// double-quoted form sidesteps that path entirely and is stable
// across yaml.v3 versions.
func (c PostgresV3Cell) MarshalYAML() (any, error) {
switch v := c.Value.(type) {
case []byte:
return &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!binary",
Value: base64.StdEncoding.EncodeToString(v),
}, nil
case string:
if StringNeedsDoubleQuoted(v) {
return &yaml.Node{
Kind: yaml.ScalarNode,
Style: yaml.DoubleQuotedStyle,
Value: v,
}, nil
}
return v, nil
case pgtype.Numeric:
return marshalPgNumericYAML(v)
case pgtype.Interval:
return marshalPgIntervalYAML(v), nil
case pgtype.Time:
return marshalPgTimeYAML(v), nil
case pgtype.Bits:
return marshalPgBitsYAML(v), nil
case pgtype.Point:
return marshalPgPointYAML(v), nil
case pgtype.Line:
return marshalPgLineYAML(v), nil
case pgtype.Lseg:
return marshalPgLsegYAML(v), nil
case pgtype.Box:
return marshalPgBoxYAML(v), nil
case pgtype.Path:
return marshalPgPathYAML(v), nil
case pgtype.Polygon:
return marshalPgPolygonYAML(v), nil
case pgtype.Circle:
return marshalPgCircleYAML(v), nil
case pgtype.TID:
return marshalPgTIDYAML(v), nil
case pgtype.TSVector:
return marshalPgTSVectorYAML(v), nil
case pgtype.Hstore:
return marshalPgHstoreYAML(v), nil
case pgtype.Range[any]:
return marshalPgRangeYAML(v)
case pgtype.Multirange[pgtype.Range[any]]:
return marshalPgMultirangeYAML(v)
case netip.Prefix:
// PG `inet` / `cidr`. Without explicit dispatch, yaml.v3's
// reflective encoder emits this as a plain string scalar
// (netip.Prefix implements encoding.TextMarshaler) — which
// round-trips to a Go `string` on decode, losing the type
// fidelity the integrations-side codec needs to dispatch
// `inet`/`cidr` encode plans. Emit a single-key mapping
// `{prefix: "<canonical>"}` so the decode side recovers the
// concrete type via the same canonical-key-set probe used
// for the 16 pgtype shapes.
return marshalNetipPrefixYAML(v), nil
case net.HardwareAddr:
// PG `macaddr` / `macaddr8`. HardwareAddr is a *named*
// `[]byte`, so the `case []byte:` arm above doesn't match
// (Go type-switch matches the unnamed type only). Without
// explicit dispatch yaml.v3 emits it via the generic slice
// encoder as a sequence of ints, which round-trips to
// `[]any` and breaks the codec's encode-plan dispatch for
// `macaddr`. Emit a single-key mapping
// `{macaddr: !!binary <base64>}` so the decode side
// recovers the named type.
return marshalHardwareAddrYAML(v), nil
case [16]uint8:
// PG `uuid` scanned into a `[16]byte`-shaped destination
// (the underlying type of uuid.UUID, and what pgx's
// UUIDArrayCodec hands back for each `uuid[]` element).
// Without explicit dispatch yaml.v3 reflects on the fixed
// array and emits it as a sequence of ints, which decodes
// back as `[]any{int64...}` — same fidelity-loss class as
// HardwareAddr, breaking the codec's encode-plan dispatch
// for `uuid`. Emit a single-key mapping
// `{uuid: !!binary <base64>}` so the decode side recovers
// the fixed-array shape symmetrically with the gob path.
return marshalUUIDBytesYAML(v), nil
case []any, map[string]any:
// An array cell (bytea[], a multi-dim array), a record, or a
// json/jsonb document: yaml.v3's reflective encoder writes it as
// before, but for a []byte element or value at any depth, which it
// would write as a sequence of ints that reads back as ints (no
// replay can send them as the bytes recorded). Such an element is
// written as !!binary, as a []byte cell is, and read back as bytes
// (yamlBinaryRestored); a uuid ([16]uint8, as pgx scans a uuid[]
// element), macaddr (net.HardwareAddr) or bit string (pgtype.Bits)
// element likewise, in the shape a cell of its type is written.
return yamlBinaryElems(v), nil
}
return c.Value, nil
}
// yamlBinaryElems is v, an array, record or document cell's value, with
// each []byte in it at any depth replaced by its !!binary node, and each
// [16]uint8, net.HardwareAddr and pgtype.Bits by its mapping (as a cell of
// its type is written: marshalUUIDBytesYAML, marshalHardwareAddrYAML,
// marshalPgBitsYAML), and everything else as it is: a container that
// holds none of them is itself.
func yamlBinaryElems(v any) any {
if !holdsBytes(v) {
return v
}
switch x := v.(type) {
case []byte:
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!binary", Value: base64.StdEncoding.EncodeToString(x)}
case [16]uint8:
return marshalUUIDBytesYAML(x)
case net.HardwareAddr:
return marshalHardwareAddrYAML(x)
case pgtype.Bits:
return marshalPgBitsYAML(x)
case []any:
out := make([]any, len(x))
for i, e := range x {
out[i] = yamlBinaryElems(e)
}
return out
case map[string]any:
out := make(map[string]any, len(x))
for k, e := range x {
out[k] = yamlBinaryElems(e)
}
return out
}
return v
}
// holdsBytes reports whether v is a []byte, [16]uint8, net.HardwareAddr or
// pgtype.Bits, or an array or document holding one at any depth.
func holdsBytes(v any) bool {
switch x := v.(type) {
case []byte, [16]uint8, net.HardwareAddr, pgtype.Bits:
return true
case []any:
for _, e := range x {
if holdsBytes(e) {
return true
}
}
case map[string]any:
for _, e := range x {
if holdsBytes(e) {
return true
}
}
}
return false
}
// YAML local tags for the pgtype-typed cell shapes. The current
// MarshalYAML emits *untagged* mappings (matching the released
// keploy's reflection-based emit, which is what cross-version replay
// in the GHA matrix encounters on Docker Hub); the primary read path
// is decodePgUntaggedMapping below, which probes the canonical key
// set. These tags exist for backward-compat decode of any recordings
// from the brief window when MarshalYAML did emit them — see
// decodePgTaggedNode.
const (
pgYAMLTagNumeric = "!pg/numeric"
pgYAMLTagInterval = "!pg/interval"
pgYAMLTagTime = "!pg/time"
pgYAMLTagBits = "!pg/bits"
pgYAMLTagPoint = "!pg/point"
pgYAMLTagLine = "!pg/line"
pgYAMLTagLseg = "!pg/lseg"
pgYAMLTagBox = "!pg/box"
pgYAMLTagPath = "!pg/path"
pgYAMLTagPolygon = "!pg/polygon"
pgYAMLTagCircle = "!pg/circle"
pgYAMLTagTID = "!pg/tid"
pgYAMLTagTSVector = "!pg/tsvector"
pgYAMLTagHstore = "!pg/hstore"
pgYAMLTagRange = "!pg/range"
pgYAMLTagMultirange = "!pg/multirange"
pgYAMLTagPrefix = "!pg/prefix"
pgYAMLTagMACAddr = "!pg/macaddr"
)
// scalarBoolNode / scalarIntNode / scalarStrNode build the lightweight
// yaml.Node primitives used to assemble the pgtype mapping bodies. Pulled
// out so each per-type MarshalYAML helper stays a flat list of key/value
// pairs and the field encoding stays uniform with how the reflective
// encoder would emit a struct field of the same Go type.
func scalarBoolNode(b bool) *yaml.Node {
v := "false"
if b {
v = "true"
}
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!bool", Value: v}
}
func scalarIntNode(n int64) *yaml.Node {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: fmt.Sprintf("%d", n)}
}
func scalarFloatNode(f float64) *yaml.Node {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!float", Value: fmt.Sprintf("%g", f)}
}
func scalarStrNode(s string) *yaml.Node {
// Same hazard as models.stringNode: an explicit !!str tag makes yaml.v3
// hard-fail on invalid UTF-8 ("cannot marshal invalid UTF-8 data as
// !!str"), which surfaces as a failed mock insert. Reachable here from
// hstore values and TSVector lexemes carrying non-UTF-8 bytes. !!binary
// stores them losslessly and yaml.v3 decodes it back on read.
if !utf8.ValidString(s) {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!binary", Value: base64.StdEncoding.EncodeToString([]byte(s))}
}
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: s}
}
func scalarKeyNode(s string) *yaml.Node {
// Mapping KEYS need the same non-UTF-8 handling as values (scalarStrNode),
// but for a different reason: an untagged key never aborts the encoder, so
// nothing here is load-bearing for the recording surviving. What bites is
// the READ side, and decodeYAMLScalarKey is what fixes that.
//
// This arm only makes the on-disk tag explicit instead of leaving it to
// yaml.v3's auto-binarize-on-emit behaviour — the file is identical either
// way today. It is kept so the encoding is stated by this package rather
// than inherited from an emitter heuristic that a library upgrade could
// change underneath us.
if !utf8.ValidString(s) {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!binary", Value: base64.StdEncoding.EncodeToString([]byte(s))}
}
return &yaml.Node{Kind: yaml.ScalarNode, Value: s}
}
// decodeYAMLScalarKey returns a mapping key's real bytes, undoing the !!binary
// encoding scalarKeyNode applies to non-UTF-8 keys.
func decodeYAMLScalarKey(k *yaml.Node) string {
if k != nil && k.Tag == "!!binary" {
if raw, err := base64.StdEncoding.DecodeString(stripBase64Whitespace(k.Value)); err == nil {
return string(raw)
}
}
if k == nil {
return ""
}
return k.Value
}
func marshalVec2Node(v pgtype.Vec2) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("x"), scalarFloatNode(v.X),
scalarKeyNode("y"), scalarFloatNode(v.Y),
},
}
}
func marshalVec2SeqNode(ps []pgtype.Vec2) *yaml.Node {
out := &yaml.Node{Kind: yaml.SequenceNode}
out.Content = make([]*yaml.Node, 0, len(ps))
for _, p := range ps {
out.Content = append(out.Content, marshalVec2Node(p))
}
return out
}
// marshalPgNumericYAML emits Numeric as `{int: "<digits>", exp: N,
// nan: bool, infinitymodifier: N, valid: bool}`. Int is rendered as a
// string scalar (matching what yaml.v3 does for *big.Int via its
// TextMarshaler), and an absent Int is emitted as `int: ""` so the
// nil-vs-zero distinction round-trips. NaN / ±infinity numerics
// legitimately carry Int=nil and the codec on the integrations side
// dispatches on that nil-ness when re-emitting.
func marshalPgNumericYAML(v pgtype.Numeric) (*yaml.Node, error) {
intStr := ""
if v.Int != nil {
intStr = v.Int.String()
}
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("int"), {Kind: yaml.ScalarNode, Tag: "!!str", Style: yaml.DoubleQuotedStyle, Value: intStr},
scalarKeyNode("exp"), scalarIntNode(int64(v.Exp)),
scalarKeyNode("nan"), scalarBoolNode(v.NaN),
scalarKeyNode("infinitymodifier"), scalarIntNode(int64(v.InfinityModifier)),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}, nil
}
func marshalPgIntervalYAML(v pgtype.Interval) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("microseconds"), scalarIntNode(v.Microseconds),
scalarKeyNode("days"), scalarIntNode(int64(v.Days)),
scalarKeyNode("months"), scalarIntNode(int64(v.Months)),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgTimeYAML(v pgtype.Time) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("microseconds"), scalarIntNode(v.Microseconds),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgBitsYAML(v pgtype.Bits) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("bytes"), {Kind: yaml.ScalarNode, Tag: "!!binary", Value: base64.StdEncoding.EncodeToString(v.Bytes)},
scalarKeyNode("len"), scalarIntNode(int64(v.Len)),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgPointYAML(v pgtype.Point) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2Node(v.P),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgLineYAML(v pgtype.Line) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("a"), scalarFloatNode(v.A),
scalarKeyNode("b"), scalarFloatNode(v.B),
scalarKeyNode("c"), scalarFloatNode(v.C),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgLsegYAML(v pgtype.Lseg) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2SeqNode(v.P[:]),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgBoxYAML(v pgtype.Box) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2SeqNode(v.P[:]),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgPathYAML(v pgtype.Path) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2SeqNode(v.P),
scalarKeyNode("closed"), scalarBoolNode(v.Closed),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgPolygonYAML(v pgtype.Polygon) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2SeqNode(v.P),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgCircleYAML(v pgtype.Circle) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("p"), marshalVec2Node(v.P),
scalarKeyNode("r"), scalarFloatNode(v.R),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgTIDYAML(v pgtype.TID) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("blocknumber"), scalarIntNode(int64(v.BlockNumber)),
scalarKeyNode("offsetnumber"), scalarIntNode(int64(v.OffsetNumber)),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
func marshalPgTSVectorYAML(v pgtype.TSVector) *yaml.Node {
lexemes := &yaml.Node{Kind: yaml.SequenceNode}
lexemes.Content = make([]*yaml.Node, 0, len(v.Lexemes))
for _, lex := range v.Lexemes {
positions := &yaml.Node{Kind: yaml.SequenceNode}
positions.Content = make([]*yaml.Node, 0, len(lex.Positions))
for _, p := range lex.Positions {
positions.Content = append(positions.Content, &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("position"), scalarIntNode(int64(p.Position)),
scalarKeyNode("weight"), scalarIntNode(int64(p.Weight)),
},
})
}
lexemes.Content = append(lexemes.Content, &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("word"), scalarStrNode(lex.Word),
scalarKeyNode("positions"), positions,
},
})
}
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("lexemes"), lexemes,
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}
}
// marshalPgHstoreYAML emits an hstore as an untagged mapping carrying
// the raw key/value pairs. SQL-NULL values inside the hstore (the
// `*string` being nil) are emitted as YAML null so the round-trip
// preserves the nil-vs-empty-string distinction the codec dispatches
// on.
func marshalPgHstoreYAML(v pgtype.Hstore) *yaml.Node {
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
out := &yaml.Node{Kind: yaml.MappingNode}
out.Content = make([]*yaml.Node, 0, 2*len(keys))
for _, k := range keys {
val := v[k]
var valNode *yaml.Node
if val == nil {
valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}
} else if StringNeedsDoubleQuoted(*val) {
valNode = &yaml.Node{Kind: yaml.ScalarNode, Style: yaml.DoubleQuotedStyle, Value: *val}
} else {
valNode = scalarStrNode(*val)
}
out.Content = append(out.Content, scalarKeyNode(k), valNode)
}
return out
}
// marshalPgRangeYAML emits Range[any] as an untagged mapping. Lower and
// Upper recurse through PostgresV3Cell.MarshalYAML so the bound element
// type stays per-cohort (int4 → int32, tstzrange → time.Time, numrange
// → pgtype.Numeric, etc.) — same recursion pattern as the gob path.
func marshalPgRangeYAML(v pgtype.Range[any]) (*yaml.Node, error) {
lowerAny, err := PostgresV3Cell{Value: v.Lower}.MarshalYAML()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.MarshalYAML range lower: %w", err)
}
upperAny, err := PostgresV3Cell{Value: v.Upper}.MarshalYAML()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.MarshalYAML range upper: %w", err)
}
lowerNode, err := toYAMLNode(lowerAny)
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.MarshalYAML range lower node: %w", err)
}
upperNode, err := toYAMLNode(upperAny)
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.MarshalYAML range upper node: %w", err)
}
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("lower"), lowerNode,
scalarKeyNode("upper"), upperNode,
scalarKeyNode("lowertype"), scalarIntNode(int64(v.LowerType)),
scalarKeyNode("uppertype"), scalarIntNode(int64(v.UpperType)),
scalarKeyNode("valid"), scalarBoolNode(v.Valid),
},
}, nil
}
func marshalPgMultirangeYAML(v pgtype.Multirange[pgtype.Range[any]]) (*yaml.Node, error) {
out := &yaml.Node{Kind: yaml.SequenceNode}
out.Content = make([]*yaml.Node, 0, len(v))
for i, r := range v {
n, err := marshalPgRangeYAML(r)
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.MarshalYAML multirange[%d]: %w", i, err)
}
out.Content = append(out.Content, n)
}
return out, nil
}
// marshalNetipPrefixYAML emits a netip.Prefix as a single-key mapping
// `{prefix: "<canonical>"}`. The canonical string form
// (`Prefix.String()`) round-trips losslessly through
// `netip.ParsePrefix` for both IPv4 and IPv6, including host-only
// forms like `1.2.3.4/32` and `::1/128`. The single-key mapping is
// unambiguous against the other pgtype shapes (none use a sole
// `prefix` key), so the canonical-key-set probe in
// decodePgUntaggedMapping picks it up cleanly without a `!pg/<name>`
// tag (cross-version compat with released keploy).
func marshalNetipPrefixYAML(v netip.Prefix) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("prefix"), scalarStrNode(v.String()),
},
}
}
// marshalHardwareAddrYAML emits a net.HardwareAddr as a single-key
// mapping `{macaddr: !!binary <base64>}`. The raw bytes (rather than
// the canonical colon-separated text form) preserve the macaddr8 vs
// macaddr distinction by length without a separate field, matching
// how the gob path stores them. The single-key mapping disambiguates
// against the unnamed `[]byte` cell, which goes through the
// `case []byte:` arm in MarshalYAML and emits a bare `!!binary`
// scalar — so the cell-level decode never confuses a `bytea` column
// with a `macaddr` column.
func marshalHardwareAddrYAML(v net.HardwareAddr) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("macaddr"), {
Kind: yaml.ScalarNode,
Tag: "!!binary",
Value: base64.StdEncoding.EncodeToString(v),
},
},
}
}
// marshalUUIDBytesYAML emits a `[16]uint8` as a single-key mapping
// `{uuid: !!binary <base64>}`. Same shape rationale as
// marshalHardwareAddrYAML: the single-key mapping disambiguates
// against the unnamed `[]byte` cell (which goes through the
// `case []byte:` arm and emits a bare `!!binary` scalar) so the
// cell-level decode never confuses a `bytea` column with a `uuid`
// column. The 16-byte length is implicit in the payload.
func marshalUUIDBytesYAML(v [16]uint8) *yaml.Node {
return &yaml.Node{
Kind: yaml.MappingNode,
Content: []*yaml.Node{
scalarKeyNode("uuid"), {
Kind: yaml.ScalarNode,
Tag: "!!binary",
Value: base64.StdEncoding.EncodeToString(v[:]),
},
},
}
}
// toYAMLNode converts a value (which may already be a *yaml.Node from a
// nested MarshalYAML call, or a plain Go value yaml.v3 will encode via
// reflection) to a *yaml.Node. Used by the range/multirange marshallers
// that need to splice the per-bound MarshalYAML output back into a
// MappingNode.Content slice.
func toYAMLNode(v any) (*yaml.Node, error) {
if n, ok := v.(*yaml.Node); ok {
return n, nil
}
out := &yaml.Node{}
if err := out.Encode(v); err != nil {
return nil, err
}
return out, nil
}
// PostgresV3SafeString wraps string for fields that may carry text
// containing embedded newlines, tabs, or other whitespace that yaml.v3
// v3.0.1's emitter mishandles when the value lives inside a sequence
// or nested mapping. The custom MarshalYAML routes problematic values
// through a DoubleQuotedStyle scalar (escapes \t / \n) so the document
// the recorder writes always re-parses on the replayer side. Use this
// type for any v3 schema field that holds free-form server- or user-
// generated content (SQL text, NoticeResponse messages, user-supplied
// table data going into mocks). Fields like enum-like Severity / Code
// don't strictly need it but using it consistently across the Notice
// shape keeps the type contract uniform.
type PostgresV3SafeString string
// MarshalYAML emits the string with DoubleQuotedStyle when the value
// would trip yaml.v3's plain/block-style emitter; otherwise emits as
// a plain scalar so common short values stay greppable.
func (s PostgresV3SafeString) MarshalYAML() (any, error) {
v := string(s)
if StringNeedsDoubleQuoted(v) {
return &yaml.Node{
Kind: yaml.ScalarNode,
Style: yaml.DoubleQuotedStyle,
Value: v,
}, nil
}
return v, nil
}
// UnmarshalYAML accepts both plain and quoted scalars — the on-disk
// shape is just a string, only the WRITE side cares about styling.
// Non-scalar nodes (sequences / mappings) are rejected with an
// explicit error: their .Value is empty, so the previous "just copy
// node.Value" path would have silently produced "" and corrupted
// notice / error fields without surfacing the malformed input.
func (s *PostgresV3SafeString) UnmarshalYAML(node *yaml.Node) error {
if node == nil {
*s = ""
return nil
}
if node.Kind != yaml.ScalarNode {
return fmt.Errorf("PostgresV3SafeString: expected scalar node, got kind=%d", node.Kind)
}
*s = PostgresV3SafeString(node.Value)
return nil
}
// StringNeedsDoubleQuoted reports whether yaml.v3's plain/block style
// heuristic would mis-emit a round-trip-unsafe representation for s.
// Reproduced in isolation against yaml.v3 v3.0.1, the landmines are:
//
// - any string containing an embedded newline picks a literal block
// scalar (|…) whose continuation-line indentation indicator
// yaml.v3 itself sometimes mis-computes inside a sequence (the
// shape PostgresV3Cells produces), so the same document fails to
// re-parse with "did not find expected key" or "found a tab
// character where an indentation space is expected";
//
// - strings starting with whitespace (space/tab/newline/CR) trip
// the indent-indicator race even when single-line, because the
// plain-style scanner chops the leading whitespace and the parser
// then can't recover the original;
//
// - strings with a trailing space or tab lose the trailing byte
// under plain style (yaml.v3 trims them).
//
// Forcing double-quoted style escapes \t / \n / leading-whitespace as
// C-style sequences and makes the emitted YAML round-trip cleanly,
// regardless of whether the scalar lives at the top level or under a
// sequence. The trade-off is readability — quoted strings are slightly
// noisier than plain — but those strings already had control bytes,
// so the loss is small in practice.
func StringNeedsDoubleQuoted(s string) bool {
if s == "" {
return false
}
switch s[0] {
case ' ', '\t', '\n', '\r':
return true
}
switch s[len(s)-1] {
case ' ', '\t':
return true
}
for i := 0; i < len(s); i++ {
switch s[i] {
case '\t', '\n', '\r':
return true
}
}
return false
}
// UnmarshalYAML decodes a YAML node into Value. Tag-driven type
// dispatch ensures ints come out as int64 (not Go's platform-
// dependent int), strings as string, etc., so downstream codec
// dispatch is uniform.
//
// !!null → nil
// !!bool → bool
// !!int → int64
// !!float → float64
// !!str → string
// !!binary → []byte
// !!timestamp → time.Time
// other → interface{} default; in a sequence or a string-keyed
// mapping, each !!binary element or value → []byte, and
// each !!binary-holding uuid, macaddr or bit string
// mapping → its type (yamlBinaryRestored)
func (c *PostgresV3Cell) UnmarshalYAML(node *yaml.Node) error {
if node == nil {
c.Value = nil
return nil
}
if node.Kind == yaml.ScalarNode {
switch node.Tag {
case "!!null":
c.Value = nil
return nil
case "!!bool":
var b bool
if err := node.Decode(&b); err != nil {
return err
}
c.Value = b
return nil
case "!!int":
var n int64
if err := node.Decode(&n); err != nil {
return err
}
c.Value = n
return nil
case "!!float":
var f float64
if err := node.Decode(&f); err != nil {
return err
}
c.Value = f
return nil
case "!!str":
// Legacy pre-PostgresV3Cell recordings stored SQL NULL as
// the printable sentinel "~~KEPLOY_PG_NULL~~" inside the
// [][]string Rows shape (earlier revisions used
// "\x00NULL\x00", but yaml.v3 rejects NUL bytes so the
// printable form shipped). When the new binary reads those
// old mocks for backward compatibility, translate the
// sentinel back to a proper NULL — otherwise every legacy
// NULL cell would collapse to a real string of the
// sentinel's text, corrupting row-level comparisons.
if node.Value == legacyNullSentinel {
c.Value = nil
return nil
}
c.Value = node.Value
return nil
case "!!binary":
// yaml.v3 exposes the base64 payload as node.Value for
// !!binary scalars; decoding into []byte directly errors
// with "cannot unmarshal !!binary into []uint8" because
// the resolver doesn't round-trip its own tag into a
// concrete Go type. Decode the base64 ourselves.
//
// yaml.v3 folds long !!binary payloads onto continuation
// lines, and the emitter keeps the embedded newlines in
// node.Value when the line exceeds ~80 columns. StdEncoding
// rejects those, so strip ASCII whitespace (space, tab,
// CR, LF) before decoding. yaml.v3 itself never inserts
// anything outside that set into the scalar, and real
// base64 payloads never contain those characters either.
raw := node.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
b, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return fmt.Errorf("PostgresV3Cell: !!binary base64 decode: %w", err)
}
c.Value = b
return nil
case "!!timestamp":
var t time.Time
if err := node.Decode(&t); err != nil {
return err
}
c.Value = t
return nil
}
// Untagged scalar: the resolver picks the type based on content.
// Empty untagged scalar in plain style (Style==0) is NULL per
// YAML 1.2. yaml.v3 has no exported PlainStyle constant; style
// value 0 is the "no explicit style" (plain) encoding.
if node.Value == "" && node.Style == 0 {
c.Value = nil
return nil
}
// Legacy NULL sentinel (see !!str branch above) when the node
// came through un-tagged — e.g. read from a bare `- ~~KEPLOY_PG_NULL~~`
// entry in a legacy Rows fixture. Same translation applies.
if node.Value == legacyNullSentinel {
c.Value = nil
return nil
}
}
// Mapping nodes are the serialized form of PostgresV3CellRaw — the
// recorder's MarshalYAML returns the struct directly, which yaml.v3
// encodes as `{format: N, bytes: [...]}`. Without this reconstruction
// the default path below decodes into `map[string]any`, which then
// trips the gob default-stringify branch when the agent streams the
// cell to the proxy: the live client sees a string whose content is
// the fmt.Sprint of the map, the codec rejects it, emitDataRow emits
// 0 bytes, and pgjdbc crashes in ByteConverter.int8 with
// ArrayIndexOutOfBoundsException.
if node.Kind == yaml.MappingNode && isPostgresV3CellRawNode(node) {
var raw PostgresV3CellRaw
if err := node.Decode(&raw); err != nil {
return fmt.Errorf("PostgresV3Cell: decode raw-cell node: %w", err)
}
c.Value = raw
return nil
}
// Backward-compat: a `!pg/<name>` local tag on the node selects a
// specific reconstructor without depending on key-set heuristics.
// MarshalYAML no longer emits these — the cross-version GHA matrix
// (PR-built recorder, released-keploy replayer) cannot tolerate
// custom tags the released binary doesn't register — but recordings
// from the brief window when the tag-driven encoder shipped still
// decode through this path.
if v, ok, err := decodePgTaggedNode(node); err != nil {
return err
} else if ok {
c.Value = v
return nil
}
// Primary read path: untagged mapping that looks like one of the
// pgtype shapes (the on-disk shape released keploy emits and the
// shape this version emits since the !pg/<name> tags were dropped).
// Probe the canonical key set and route to the right reconstructor;
// if nothing matches, fall through to the generic any-decode path.
//
// `!!map` is yaml.v3's auto-resolved tag for any plain mapping
// without an explicit local tag — that's the shape current and
// pre-tagged recordings both carry on disk, so we treat it the
// same as an empty tag string for the canonical-key-set probe.
if node.Kind == yaml.MappingNode && (node.Tag == "" || node.Tag == "!!map") {
if v, ok, err := decodePgUntaggedMapping(node); err != nil {
return err
} else if ok {
c.Value = v
return nil
}
}
var v any
if err := node.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell: decode node (kind=%d, tag=%q): %w", node.Kind, node.Tag, err)
}
c.Value = yamlBinaryRestored(node, v)
return nil
}
// yamlBinaryRestored returns v, the generic decode of node (an array,
// record or document cell), with each element or value written as !!binary
// in it (yamlBinaryElems) the bytes it encodes, where yaml.v3's generic
// decode makes text of them, and each {uuid}, {macaddr} or {bytes, len,
// valid} mapping holding its bytes as !!binary the [16]uint8,
// net.HardwareAddr or pgtype.Bits it encodes. Everything else is as the
// generic decode made it. An older mock's element, written as a sequence
// of ints (or a mapping of them), is what it was stored as, as before.
func yamlBinaryRestored(node *yaml.Node, v any) any {
for depth := 0; node.Kind == yaml.AliasNode && node.Alias != nil && depth < 16; depth++ {
node = node.Alias
}
switch node.Kind {
case yaml.ScalarNode:
if node.Tag != "!!binary" {
return v
}
raw := node.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
if b, err := base64.StdEncoding.DecodeString(raw); err == nil {
return b
}
case yaml.SequenceNode:
if s, ok := v.([]any); ok && len(s) == len(node.Content) {
for i := range s {
s[i] = yamlBinaryRestored(node.Content[i], s[i])
}
}
case yaml.MappingNode:
// A uuid, macaddr or bit string element, as yamlBinaryElems
// writes it: each holds its bytes as !!binary, which a JSON
// document never does, so the shape is no document's.
switch {
case len(node.Content) == 2 && isUUIDBytesMapping(node):
if u, err := decodeUUIDBytesMapping(node); err == nil {
return u
}
case binaryFieldMapping(node, "macaddr"):
if a, err := decodeHardwareAddrMapping(node); err == nil {
return a
}
case binaryFieldMapping(node, "bytes", "len", "valid"):
if b, err := decodePgBitsMapping(node); err == nil {
return b
}
}
if m, ok := v.(map[string]any); ok {
for i := 0; i+1 < len(node.Content); i += 2 {
k := node.Content[i]
if k.Kind != yaml.ScalarNode || k.Tag == "!!merge" {
continue
}
if e, ok := m[k.Value]; ok {
m[k.Value] = yamlBinaryRestored(node.Content[i+1], e)
}
}
}
}
return v
}
// decodePgTaggedNode handles the `!pg/<name>` tag dispatch. Returns
// (value, true, nil) on a match, (nil, false, nil) if the node has a
// different (or empty) tag, or (nil, false, err) on a decode failure.
func decodePgTaggedNode(node *yaml.Node) (any, bool, error) {
switch node.Tag {
case pgYAMLTagNumeric:
v, err := decodePgNumericMapping(node)
return v, true, err
case pgYAMLTagInterval:
v, err := decodePgIntervalMapping(node)
return v, true, err
case pgYAMLTagTime:
v, err := decodePgTimeMapping(node)
return v, true, err
case pgYAMLTagBits:
v, err := decodePgBitsMapping(node)
return v, true, err
case pgYAMLTagPoint:
v, err := decodePgPointMapping(node)
return v, true, err
case pgYAMLTagLine:
v, err := decodePgLineMapping(node)
return v, true, err
case pgYAMLTagLseg:
v, err := decodePgLsegMapping(node)
return v, true, err
case pgYAMLTagBox:
v, err := decodePgBoxMapping(node)
return v, true, err
case pgYAMLTagPath:
v, err := decodePgPathMapping(node)
return v, true, err
case pgYAMLTagPolygon:
v, err := decodePgPolygonMapping(node)
return v, true, err
case pgYAMLTagCircle:
v, err := decodePgCircleMapping(node)
return v, true, err
case pgYAMLTagTID:
v, err := decodePgTIDMapping(node)
return v, true, err
case pgYAMLTagTSVector:
v, err := decodePgTSVectorMapping(node)
return v, true, err
case pgYAMLTagHstore:
v, err := decodePgHstoreMapping(node)
return v, true, err
case pgYAMLTagRange:
v, err := decodePgRangeMapping(node)
return v, true, err
case pgYAMLTagMultirange:
v, err := decodePgMultirangeNode(node)
return v, true, err
case pgYAMLTagPrefix:
v, err := decodeNetipPrefixMapping(node)
return v, true, err
case pgYAMLTagMACAddr:
v, err := decodeHardwareAddrMapping(node)
return v, true, err
}
return nil, false, nil
}
// decodePgUntaggedMapping inspects an untagged MappingNode for one of
// the canonical pgtype key sets and routes to the matching
// reconstructor. This is the primary read path: MarshalYAML emits
// untagged mappings (for cross-version compat with released keploy
// on Docker Hub, whose YAML library doesn't know the `!pg/<name>`
// local tags), and listmonk's pre-fix mocks.yaml shape — Numeric as a
// bare `{int, exp, nan, infinitymodifier, valid}` mapping — flows
// through the same path. Only key sets that uniquely identify a
// single pgtype shape are probed; ambiguous shapes (Point/Lseg/Box/
// Polygon all match `{p, valid}`) cannot be recovered to their
// concrete Go type from an untagged mapping alone — those decode as
// `map[string]any`, matching how released keploy's reflection emit
// also rehydrates them.
//
// Invariant: every key set in the switch below must be pairwise
// distinct from — and not a subset of — every other key set in the
// switch. `keysEqual` requires exact equality, so a subset relation
// (e.g. Time's {microseconds, valid} is contained in Interval's
// {microseconds, days, months, valid}) does not cause silent
// corruption today; but the moment anyone weakens the predicate to a
// "contains-all-required-keys" match, the smaller set would shadow
// the larger one and silently misroute the larger type's recordings.
// The disjointness audit is pinned by
// TestPgtypeYAMLKeySetsMutuallyDisjoint in
// pgtype_yaml_disambiguation_test.go.
func decodePgUntaggedMapping(node *yaml.Node) (any, bool, error) {
keys := pgMappingKeySet(node)
if keys == nil {
return nil, false, nil
}
switch {
case keysEqual(keys, []string{"int", "exp", "nan", "infinitymodifier", "valid"}) && yamlFieldsAre(node, yamlNumericFields):
v, err := decodePgNumericMapping(node)
return v, true, err
case keysEqual(keys, []string{"microseconds", "days", "months", "valid"}) && yamlFieldsAre(node, yamlIntervalFields):
v, err := decodePgIntervalMapping(node)
return v, true, err
case keysEqual(keys, []string{"microseconds", "valid"}) && yamlFieldsAre(node, yamlTimeFields):
v, err := decodePgTimeMapping(node)
return v, true, err
case keysEqual(keys, []string{"bytes", "len", "valid"}) && yamlFieldsAre(node, yamlBitsFields):
v, err := decodePgBitsMapping(node)
return v, true, err
case keysEqual(keys, []string{"a", "b", "c", "valid"}) && yamlFieldsAre(node, yamlLineFields):
v, err := decodePgLineMapping(node)
return v, true, err
case keysEqual(keys, []string{"p", "closed", "valid"}) && yamlFieldsAre(node, yamlPathFields):
v, err := decodePgPathMapping(node)
return v, true, err
case keysEqual(keys, []string{"p", "r", "valid"}) && yamlFieldsAre(node, yamlCircleFields):
v, err := decodePgCircleMapping(node)
return v, true, err
case keysEqual(keys, []string{"blocknumber", "offsetnumber", "valid"}) && yamlFieldsAre(node, yamlTIDFields):
v, err := decodePgTIDMapping(node)
return v, true, err
case keysEqual(keys, []string{"lexemes", "valid"}) && yamlFieldsAre(node, yamlTSVectorFields):
v, err := decodePgTSVectorMapping(node)
return v, true, err
case keysEqual(keys, []string{"lower", "upper", "lowertype", "uppertype", "valid"}) && yamlFieldsAre(node, yamlRangeFields):
v, err := decodePgRangeMapping(node)
return v, true, err
case keysEqual(keys, []string{"prefix"}):
// A {prefix} or {macaddr} mapping is the cell's only with the
// value its marshaller writes, a prefix's text or !!binary: an
// application's jsonb document can have the key too (a
// {"prefix": "img_"}, a {"macaddr": "08:00:2b:01:02:03"}), and is
// the document, where it used to fail the mock's load.
if !isNetipPrefixMapping(node) {
return nil, false, nil
}
v, err := decodeNetipPrefixMapping(node)
return v, true, err
case keysEqual(keys, []string{"macaddr"}):
if !binaryFieldMapping(node, "macaddr") {
return nil, false, nil
}
v, err := decodeHardwareAddrMapping(node)
return v, true, err
case keysEqual(keys, []string{"uuid"}):
// Tightened dispatch: `{uuid: ...}` is a much more common key
// in user-shaped JSONB than `{macaddr}` or `{prefix}`, so an
// untagged-key match alone would mis-route a JSONB column
// like `{"uuid": "<text-uuid-string>"}` here, fail base64
// decode, and surface as a YAML unmarshal error that prevents
// mocks.yaml from loading at all. Require the value to
// actually be a `!!binary` payload of exactly 16 bytes (the
// shape marshalUUIDBytesYAML emits). Anything else falls
// through to the generic any-decode path so JSONB round-trips
// losslessly as `map[string]any`, matching how it would have
// before this PR added uuid dispatch.
if !isUUIDBytesMapping(node) {
return nil, false, nil
}
v, err := decodeUUIDBytesMapping(node)
return v, true, err
}
return nil, false, nil
}
// yamlKind is a set of the kinds of YAML node a pgtype shape's field is
// written as, for decodePgUntaggedMapping's yamlFieldsAre.
type yamlKind uint16
const (
yamlInt yamlKind = 1 << iota
yamlFloat
yamlBool
yamlStr
yamlBinary
yamlNull
yamlSeq
yamlMap
yamlAny = yamlInt | yamlFloat | yamlBool | yamlStr | yamlBinary | yamlNull | yamlSeq | yamlMap
)
// The kinds each shape's marshaller writes its fields as (a float field
// also an int, as a hand-edited fixture can hold; a bit string's bytes
// also a sequence of ints, as decodePgBitsMapping reads one; a numeric's
// int also a bare integer). An application's jsonb document with a
// shape's keys and other kinds of value (a {"bytes": 2048, "len": 1,
// "valid": true}, a {"microseconds": "x", "valid": "y"}) is the
// document's, where it used to fail the mock's load; a value of the right
// kind that the decoder rejects (an out-of-range field) still fails it.
var (
yamlNumericFields = map[string]yamlKind{"int": yamlStr | yamlInt | yamlNull, "exp": yamlInt, "nan": yamlBool, "infinitymodifier": yamlInt, "valid": yamlBool}
yamlIntervalFields = map[string]yamlKind{"microseconds": yamlInt, "days": yamlInt, "months": yamlInt, "valid": yamlBool}
yamlTimeFields = map[string]yamlKind{"microseconds": yamlInt, "valid": yamlBool}
yamlBitsFields = map[string]yamlKind{"bytes": yamlBinary | yamlSeq, "len": yamlInt, "valid": yamlBool}
yamlLineFields = map[string]yamlKind{"a": yamlFloat | yamlInt, "b": yamlFloat | yamlInt, "c": yamlFloat | yamlInt, "valid": yamlBool}
yamlPathFields = map[string]yamlKind{"p": yamlSeq, "closed": yamlBool, "valid": yamlBool}
yamlCircleFields = map[string]yamlKind{"p": yamlMap, "r": yamlFloat | yamlInt, "valid": yamlBool}
yamlTIDFields = map[string]yamlKind{"blocknumber": yamlInt, "offsetnumber": yamlInt, "valid": yamlBool}
yamlTSVectorFields = map[string]yamlKind{"lexemes": yamlSeq, "valid": yamlBool}
yamlRangeFields = map[string]yamlKind{"lower": yamlAny, "upper": yamlAny, "lowertype": yamlInt, "uppertype": yamlInt, "valid": yamlBool}
)
// yamlFieldsAre reports whether each field of node, a mapping, that kinds
// names holds a node of one of its kinds (any node at all, for yamlAny: a
// range's bound is whatever its value's marshaller writes, a !!timestamp
// among them).
func yamlFieldsAre(node *yaml.Node, kinds map[string]yamlKind) bool {
fields, err := pgMappingFields(node)
if err != nil {
return false
}
for k, want := range kinds {
n := fields[k]
for depth := 0; n != nil && n.Kind == yaml.AliasNode && depth < 16; depth++ {
n = n.Alias
}
if n == nil || want != yamlAny && want&yamlKindOf(n) == 0 {
return false
}
}
return true
}
// yamlKindOf is the kind of node n.
func yamlKindOf(n *yaml.Node) yamlKind {
switch n.Kind {
case yaml.SequenceNode:
return yamlSeq
case yaml.MappingNode:
return yamlMap
case yaml.ScalarNode:
switch n.ShortTag() {
case "!!int":
return yamlInt
case "!!float":
return yamlFloat
case "!!bool":
return yamlBool
case "!!str":
return yamlStr
case "!!binary":
return yamlBinary
case "!!null":
return yamlNull
}
}
return 0
}
// pgMappingKeySet returns the lowercased keys of a mapping node, or nil
// if any key is non-scalar (in which case backward-compat probing must
// not match — the node is a generic map).
func pgMappingKeySet(node *yaml.Node) []string {
if node.Kind != yaml.MappingNode {
return nil
}
out := make([]string, 0, len(node.Content)/2)
for i := 0; i < len(node.Content); i += 2 {
k := node.Content[i]
if k.Kind != yaml.ScalarNode {
return nil
}
out = append(out, k.Value)
}
return out
}
func keysEqual(got, want []string) bool {
if len(got) != len(want) {
return false
}
gs := make([]string, len(got))
copy(gs, got)
ws := make([]string, len(want))
copy(ws, want)
sort.Strings(gs)
sort.Strings(ws)
for i := range gs {
if gs[i] != ws[i] {
return false
}
}
return true
}
// pgMappingFields returns the value node for each lowercased key in a
// MappingNode. Per-type decoders look up the fields they care about
// rather than walking the slice manually for every type.
func pgMappingFields(node *yaml.Node) (map[string]*yaml.Node, error) {
if node.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expected mapping node, got kind=%d", node.Kind)
}
out := make(map[string]*yaml.Node, len(node.Content)/2)
for i := 0; i+1 < len(node.Content); i += 2 {
k := node.Content[i]
if k.Kind != yaml.ScalarNode {
return nil, fmt.Errorf("non-scalar mapping key (kind=%d)", k.Kind)
}
out[k.Value] = node.Content[i+1]
}
return out, nil
}
func decodePgNumericMapping(node *yaml.Node) (pgtype.Numeric, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric: %w", err)
}
out := pgtype.Numeric{}
if intNode, ok := fields["int"]; ok && intNode != nil {
// pgtype.Numeric.Int is *big.Int rendered via its TextMarshaler
// — yaml.v3 emits "<digits>" or "" for nil. Decode the string
// form back into *big.Int; an empty string keeps Int=nil so
// NaN / ±infinity numerics preserve their nil-Int identity.
var s string
switch {
case intNode.Kind == yaml.ScalarNode && intNode.Tag == "!!null":
// yaml.v3's reflective write of a nil *big.Int, as an
// array's NaN or infinite numeric element is written.
case intNode.Kind == yaml.ScalarNode:
s = intNode.Value
default:
if err := intNode.Decode(&s); err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric int: %w", err)
}
}
if s != "" {
bi := new(big.Int)
if _, ok := bi.SetString(s, 10); !ok {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric int: invalid integer literal %q", s)
}
out.Int = bi
}
}
if expNode, ok := fields["exp"]; ok && expNode != nil {
var n int32
if err := expNode.Decode(&n); err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric exp: %w", err)
}
out.Exp = n
}
if nanNode, ok := fields["nan"]; ok && nanNode != nil {
if err := nanNode.Decode(&out.NaN); err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric nan: %w", err)
}
}
if imNode, ok := fields["infinitymodifier"]; ok && imNode != nil {
var n int8
if err := imNode.Decode(&n); err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric infinitymodifier: %w", err)
}
out.InfinityModifier = pgtype.InfinityModifier(n)
}
if validNode, ok := fields["valid"]; ok && validNode != nil {
if err := validNode.Decode(&out.Valid); err != nil {
return pgtype.Numeric{}, fmt.Errorf("pg/numeric valid: %w", err)
}
}
return out, nil
}
func decodePgIntervalMapping(node *yaml.Node) (pgtype.Interval, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Interval{}, fmt.Errorf("pg/interval: %w", err)
}
out := pgtype.Interval{}
if n, ok := fields["microseconds"]; ok && n != nil {
if err := n.Decode(&out.Microseconds); err != nil {
return out, fmt.Errorf("pg/interval microseconds: %w", err)
}
}
if n, ok := fields["days"]; ok && n != nil {
if err := n.Decode(&out.Days); err != nil {
return out, fmt.Errorf("pg/interval days: %w", err)
}
}
if n, ok := fields["months"]; ok && n != nil {
if err := n.Decode(&out.Months); err != nil {
return out, fmt.Errorf("pg/interval months: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/interval valid: %w", err)
}
}
return out, nil
}
func decodePgTimeMapping(node *yaml.Node) (pgtype.Time, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Time{}, fmt.Errorf("pg/time: %w", err)
}
out := pgtype.Time{}
if n, ok := fields["microseconds"]; ok && n != nil {
if err := n.Decode(&out.Microseconds); err != nil {
return out, fmt.Errorf("pg/time microseconds: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/time valid: %w", err)
}
}
return out, nil
}
func decodePgBitsMapping(node *yaml.Node) (pgtype.Bits, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Bits{}, fmt.Errorf("pg/bits: %w", err)
}
out := pgtype.Bits{}
if n, ok := fields["bytes"]; ok && n != nil {
// Bytes may arrive as `!!binary` (the new tagged emitter) or as
// a `[]int` sequence (yaml.v3's reflective fallback). Try the
// binary scalar path first, then fall back to the int-slice
// decode so backward-compat with hand-edited fixtures works.
if n.Kind == yaml.ScalarNode && n.Tag == "!!binary" {
raw := n.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
b, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return out, fmt.Errorf("pg/bits bytes: %w", err)
}
out.Bytes = b
} else if err := n.Decode(&out.Bytes); err != nil {
return out, fmt.Errorf("pg/bits bytes: %w", err)
}
}
if n, ok := fields["len"]; ok && n != nil {
if err := n.Decode(&out.Len); err != nil {
return out, fmt.Errorf("pg/bits len: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/bits valid: %w", err)
}
}
return out, nil
}
func decodeVec2Mapping(node *yaml.Node) (pgtype.Vec2, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Vec2{}, fmt.Errorf("pg vec2: %w", err)
}
out := pgtype.Vec2{}
if n, ok := fields["x"]; ok && n != nil {
if err := n.Decode(&out.X); err != nil {
return out, fmt.Errorf("pg vec2 x: %w", err)
}
}
if n, ok := fields["y"]; ok && n != nil {
if err := n.Decode(&out.Y); err != nil {
return out, fmt.Errorf("pg vec2 y: %w", err)
}
}
return out, nil
}
func decodeVec2SeqNode(node *yaml.Node) ([]pgtype.Vec2, error) {
if node == nil {
return nil, nil
}
if node.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("pg vec2 seq: expected sequence, got kind=%d", node.Kind)
}
out := make([]pgtype.Vec2, 0, len(node.Content))
for i, child := range node.Content {
v, err := decodeVec2Mapping(child)
if err != nil {
return nil, fmt.Errorf("pg vec2 seq[%d]: %w", i, err)
}
out = append(out, v)
}
return out, nil
}
func decodePgPointMapping(node *yaml.Node) (pgtype.Point, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Point{}, fmt.Errorf("pg/point: %w", err)
}
out := pgtype.Point{}
if n, ok := fields["p"]; ok && n != nil {
v, err := decodeVec2Mapping(n)
if err != nil {
return out, fmt.Errorf("pg/point p: %w", err)
}
out.P = v
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/point valid: %w", err)
}
}
return out, nil
}
func decodePgLineMapping(node *yaml.Node) (pgtype.Line, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Line{}, fmt.Errorf("pg/line: %w", err)
}
out := pgtype.Line{}
if n, ok := fields["a"]; ok && n != nil {
if err := n.Decode(&out.A); err != nil {
return out, fmt.Errorf("pg/line a: %w", err)
}
}
if n, ok := fields["b"]; ok && n != nil {
if err := n.Decode(&out.B); err != nil {
return out, fmt.Errorf("pg/line b: %w", err)
}
}
if n, ok := fields["c"]; ok && n != nil {
if err := n.Decode(&out.C); err != nil {
return out, fmt.Errorf("pg/line c: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/line valid: %w", err)
}
}
return out, nil
}
func decodePgLsegMapping(node *yaml.Node) (pgtype.Lseg, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Lseg{}, fmt.Errorf("pg/lseg: %w", err)
}
out := pgtype.Lseg{}
if n, ok := fields["p"]; ok && n != nil {
ps, err := decodeVec2SeqNode(n)
if err != nil {
return out, fmt.Errorf("pg/lseg p: %w", err)
}
if len(ps) != 2 {
return out, fmt.Errorf("pg/lseg p: expected 2 vec2, got %d", len(ps))
}
out.P = [2]pgtype.Vec2{ps[0], ps[1]}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/lseg valid: %w", err)
}
}
return out, nil
}
func decodePgBoxMapping(node *yaml.Node) (pgtype.Box, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Box{}, fmt.Errorf("pg/box: %w", err)
}
out := pgtype.Box{}
if n, ok := fields["p"]; ok && n != nil {
ps, err := decodeVec2SeqNode(n)
if err != nil {
return out, fmt.Errorf("pg/box p: %w", err)
}
if len(ps) != 2 {
return out, fmt.Errorf("pg/box p: expected 2 vec2, got %d", len(ps))
}
out.P = [2]pgtype.Vec2{ps[0], ps[1]}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/box valid: %w", err)
}
}
return out, nil
}
func decodePgPathMapping(node *yaml.Node) (pgtype.Path, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Path{}, fmt.Errorf("pg/path: %w", err)
}
out := pgtype.Path{}
if n, ok := fields["p"]; ok && n != nil {
ps, err := decodeVec2SeqNode(n)
if err != nil {
return out, fmt.Errorf("pg/path p: %w", err)
}
out.P = ps
}
if n, ok := fields["closed"]; ok && n != nil {
if err := n.Decode(&out.Closed); err != nil {
return out, fmt.Errorf("pg/path closed: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/path valid: %w", err)
}
}
return out, nil
}
func decodePgPolygonMapping(node *yaml.Node) (pgtype.Polygon, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Polygon{}, fmt.Errorf("pg/polygon: %w", err)
}
out := pgtype.Polygon{}
if n, ok := fields["p"]; ok && n != nil {
ps, err := decodeVec2SeqNode(n)
if err != nil {
return out, fmt.Errorf("pg/polygon p: %w", err)
}
out.P = ps
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/polygon valid: %w", err)
}
}
return out, nil
}
func decodePgCircleMapping(node *yaml.Node) (pgtype.Circle, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Circle{}, fmt.Errorf("pg/circle: %w", err)
}
out := pgtype.Circle{}
if n, ok := fields["p"]; ok && n != nil {
v, err := decodeVec2Mapping(n)
if err != nil {
return out, fmt.Errorf("pg/circle p: %w", err)
}
out.P = v
}
if n, ok := fields["r"]; ok && n != nil {
if err := n.Decode(&out.R); err != nil {
return out, fmt.Errorf("pg/circle r: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/circle valid: %w", err)
}
}
return out, nil
}
func decodePgTIDMapping(node *yaml.Node) (pgtype.TID, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.TID{}, fmt.Errorf("pg/tid: %w", err)
}
out := pgtype.TID{}
if n, ok := fields["blocknumber"]; ok && n != nil {
if err := n.Decode(&out.BlockNumber); err != nil {
return out, fmt.Errorf("pg/tid blocknumber: %w", err)
}
}
if n, ok := fields["offsetnumber"]; ok && n != nil {
if err := n.Decode(&out.OffsetNumber); err != nil {
return out, fmt.Errorf("pg/tid offsetnumber: %w", err)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/tid valid: %w", err)
}
}
return out, nil
}
func decodePgTSVectorMapping(node *yaml.Node) (pgtype.TSVector, error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.TSVector{}, fmt.Errorf("pg/tsvector: %w", err)
}
out := pgtype.TSVector{}
if n, ok := fields["lexemes"]; ok && n != nil {
if n.Kind != yaml.SequenceNode {
return out, fmt.Errorf("pg/tsvector lexemes: expected sequence, got kind=%d", n.Kind)
}
out.Lexemes = make([]pgtype.TSVectorLexeme, 0, len(n.Content))
for i, lexNode := range n.Content {
lf, err := pgMappingFields(lexNode)
if err != nil {
return out, fmt.Errorf("pg/tsvector lex[%d]: %w", i, err)
}
lex := pgtype.TSVectorLexeme{}
if w, ok := lf["word"]; ok && w != nil {
if err := w.Decode(&lex.Word); err != nil {
return out, fmt.Errorf("pg/tsvector lex[%d] word: %w", i, err)
}
}
if pn, ok := lf["positions"]; ok && pn != nil {
if pn.Kind != yaml.SequenceNode {
return out, fmt.Errorf("pg/tsvector lex[%d] positions: expected sequence, got kind=%d", i, pn.Kind)
}
lex.Positions = make([]pgtype.TSVectorPosition, 0, len(pn.Content))
for j, posNode := range pn.Content {
pf, err := pgMappingFields(posNode)
if err != nil {
return out, fmt.Errorf("pg/tsvector lex[%d] pos[%d]: %w", i, j, err)
}
pos := pgtype.TSVectorPosition{}
if pp, ok := pf["position"]; ok && pp != nil {
if err := pp.Decode(&pos.Position); err != nil {
return out, fmt.Errorf("pg/tsvector lex[%d] pos[%d] position: %w", i, j, err)
}
}
if pw, ok := pf["weight"]; ok && pw != nil {
var w byte
if err := pw.Decode(&w); err != nil {
return out, fmt.Errorf("pg/tsvector lex[%d] pos[%d] weight: %w", i, j, err)
}
pos.Weight = pgtype.TSVectorWeight(w)
}
lex.Positions = append(lex.Positions, pos)
}
}
out.Lexemes = append(out.Lexemes, lex)
}
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/tsvector valid: %w", err)
}
}
return out, nil
}
func decodePgHstoreMapping(node *yaml.Node) (pgtype.Hstore, error) {
if node.Kind != yaml.MappingNode {
return nil, fmt.Errorf("pg/hstore: expected mapping, got kind=%d", node.Kind)
}
out := make(pgtype.Hstore, len(node.Content)/2)
for i := 0; i+1 < len(node.Content); i += 2 {
k := node.Content[i]
v := node.Content[i+1]
if k.Kind != yaml.ScalarNode {
return nil, fmt.Errorf("pg/hstore: non-scalar key (kind=%d)", k.Kind)
}
key := decodeYAMLScalarKey(k)
if v.Kind == yaml.ScalarNode && v.Tag == "!!null" {
out[key] = nil
continue
}
var s string
if err := v.Decode(&s); err != nil {
return nil, fmt.Errorf("pg/hstore value for %q: %w", key, err)
}
sv := s
out[key] = &sv
}
return out, nil
}
func decodePgRangeMapping(node *yaml.Node) (pgtype.Range[any], error) {
fields, err := pgMappingFields(node)
if err != nil {
return pgtype.Range[any]{}, fmt.Errorf("pg/range: %w", err)
}
out := pgtype.Range[any]{}
if n, ok := fields["lower"]; ok && n != nil {
var c PostgresV3Cell
if err := c.UnmarshalYAML(n); err != nil {
return out, fmt.Errorf("pg/range lower: %w", err)
}
out.Lower = c.Value
}
if n, ok := fields["upper"]; ok && n != nil {
var c PostgresV3Cell
if err := c.UnmarshalYAML(n); err != nil {
return out, fmt.Errorf("pg/range upper: %w", err)
}
out.Upper = c.Value
}
if n, ok := fields["lowertype"]; ok && n != nil {
var b byte
if err := n.Decode(&b); err != nil {
return out, fmt.Errorf("pg/range lowertype: %w", err)
}
out.LowerType = pgtype.BoundType(b)
}
if n, ok := fields["uppertype"]; ok && n != nil {
var b byte
if err := n.Decode(&b); err != nil {
return out, fmt.Errorf("pg/range uppertype: %w", err)
}
out.UpperType = pgtype.BoundType(b)
}
if n, ok := fields["valid"]; ok && n != nil {
if err := n.Decode(&out.Valid); err != nil {
return out, fmt.Errorf("pg/range valid: %w", err)
}
}
return out, nil
}
func decodePgMultirangeNode(node *yaml.Node) (pgtype.Multirange[pgtype.Range[any]], error) {
if node.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("pg/multirange: expected sequence, got kind=%d", node.Kind)
}
out := make(pgtype.Multirange[pgtype.Range[any]], 0, len(node.Content))
for i, child := range node.Content {
r, err := decodePgRangeMapping(child)
if err != nil {
return nil, fmt.Errorf("pg/multirange[%d]: %w", i, err)
}
out = append(out, r)
}
return out, nil
}
// isNetipPrefixMapping reports whether node, a {prefix} mapping, holds a
// prefix's text, as marshalNetipPrefixYAML writes it.
func isNetipPrefixMapping(node *yaml.Node) bool {
fields, err := pgMappingFields(node)
if err != nil {
return false
}
pn := fields["prefix"]
if pn == nil || pn.Kind != yaml.ScalarNode {
return false
}
_, err = netip.ParsePrefix(pn.Value)
return err == nil
}
// decodeNetipPrefixMapping reconstructs a netip.Prefix from the
// `{prefix: "<canonical>"}` mapping shape emitted by
// marshalNetipPrefixYAML. The string body is parsed with
// netip.ParsePrefix; both IPv4 (`1.2.3.0/24`, `1.2.3.4/32`) and IPv6
// (`2001:db8::/32`, `::1/128`) canonical forms round-trip cleanly.
func decodeNetipPrefixMapping(node *yaml.Node) (netip.Prefix, error) {
fields, err := pgMappingFields(node)
if err != nil {
return netip.Prefix{}, fmt.Errorf("pg/prefix: %w", err)
}
pn, ok := fields["prefix"]
if !ok || pn == nil {
return netip.Prefix{}, fmt.Errorf("pg/prefix: missing 'prefix' field")
}
if pn.Kind != yaml.ScalarNode {
return netip.Prefix{}, fmt.Errorf("pg/prefix: 'prefix' must be a scalar, got kind=%d", pn.Kind)
}
p, err := netip.ParsePrefix(pn.Value)
if err != nil {
return netip.Prefix{}, fmt.Errorf("pg/prefix: parse %q: %w", pn.Value, err)
}
return p, nil
}
// decodeHardwareAddrMapping reconstructs a net.HardwareAddr from the
// `{macaddr: !!binary <base64>}` mapping shape emitted by
// marshalHardwareAddrYAML. The byte slice is wrapped in a
// HardwareAddr named-type cast; the byte length encodes macaddr (6)
// vs macaddr8 (8) without a separate field.
func decodeHardwareAddrMapping(node *yaml.Node) (net.HardwareAddr, error) {
fields, err := pgMappingFields(node)
if err != nil {
return nil, fmt.Errorf("pg/macaddr: %w", err)
}
mn, ok := fields["macaddr"]
if !ok || mn == nil {
return nil, fmt.Errorf("pg/macaddr: missing 'macaddr' field")
}
if mn.Kind != yaml.ScalarNode {
return nil, fmt.Errorf("pg/macaddr: 'macaddr' must be a scalar, got kind=%d", mn.Kind)
}
raw := mn.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
b, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("pg/macaddr: base64 decode: %w", err)
}
return net.HardwareAddr(b), nil
}
// binaryFieldMapping reports whether node is a mapping of exactly keys
// whose first key's value is a !!binary scalar: the shape a macaddr
// ({macaddr}) or bit string ({bytes, len, valid}) cell is written in.
func binaryFieldMapping(node *yaml.Node, keys ...string) bool {
if node.Kind != yaml.MappingNode || len(node.Content) != 2*len(keys) {
return false
}
fields, err := pgMappingFields(node)
if err != nil || len(fields) != len(keys) {
return false
}
for _, k := range keys {
if _, ok := fields[k]; !ok {
return false
}
}
b := fields[keys[0]]
return b != nil && b.Kind == yaml.ScalarNode && b.Tag == "!!binary"
}
// isUUIDBytesMapping reports whether an untagged YAML mapping carries
// the exact `{uuid: !!binary <base64-of-16-bytes>}` shape that
// marshalUUIDBytesYAML emits. Used as the dispatch gate in
// decodePgUntaggedMapping so a user-shaped JSONB value like
// `{"uuid": "<text-uuid-string>"}` (a common app pattern) doesn't
// mis-route into decodeUUIDBytesMapping and surface as a YAML load
// error. The check is intentionally strict: the !!binary tag is the
// distinguishing signal between "this was emitted as a uuid cell" and
// "this is a JSONB map that happens to have a uuid key".
func isUUIDBytesMapping(node *yaml.Node) bool {
fields, err := pgMappingFields(node)
if err != nil {
return false
}
mn, ok := fields["uuid"]
if !ok || mn == nil || mn.Kind != yaml.ScalarNode {
return false
}
if mn.Tag != "!!binary" {
return false
}
raw := mn.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
b, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return false
}
return len(b) == 16
}
// decodeUUIDBytesMapping reconstructs a `[16]uint8` from the
// `{uuid: !!binary <base64>}` mapping shape emitted by
// marshalUUIDBytesYAML. The 16-byte length is enforced explicitly
// — fixed-array sizing must be exact, and a wrong-length payload
// would silently truncate or zero-pad if we just `copy`'d.
func decodeUUIDBytesMapping(node *yaml.Node) ([16]uint8, error) {
var arr [16]uint8
fields, err := pgMappingFields(node)
if err != nil {
return arr, fmt.Errorf("pg/uuid: %w", err)
}
mn, ok := fields["uuid"]
if !ok || mn == nil {
return arr, fmt.Errorf("pg/uuid: missing 'uuid' field")
}
if mn.Kind != yaml.ScalarNode {
return arr, fmt.Errorf("pg/uuid: 'uuid' must be a scalar, got kind=%d", mn.Kind)
}
raw := mn.Value
if strings.ContainsAny(raw, " \t\r\n") {
raw = stripBase64Whitespace(raw)
}
b, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return arr, fmt.Errorf("pg/uuid: base64 decode: %w", err)
}
if len(b) != 16 {
return arr, fmt.Errorf("pg/uuid: expected 16 bytes, got %d", len(b))
}
copy(arr[:], b)
return arr, nil
}
// isPostgresV3CellRawNode reports whether a YAML mapping node has
// exactly the `format` and `bytes` keys that `PostgresV3CellRaw`
// marshals to, with values of the kinds it writes and decodes: an integer
// format, and bytes as a sequence (yaml.v3 writes a []byte as one of
// ints, `[]` when empty) or null. Such a mapping is the signature of a
// raw-OID fallback cell; any other shape (e.g. future composite types,
// ad-hoc maps from custom recorders, an application's jsonb document like
// {"format": "png", "bytes": 2048}, or one holding bytes, which yaml.v3
// writes as !!binary and the raw cell's []byte does not decode, and
// which used to fail the mock's load) is left to the generic any-decode
// path. An integer format out of int16's range still fails the decode.
func isPostgresV3CellRawNode(node *yaml.Node) bool {
if node == nil || node.Kind != yaml.MappingNode || len(node.Content) != 4 {
return false
}
seenFormat, seenBytes := false, false
for i := 0; i < len(node.Content); i += 2 {
key, val := node.Content[i], node.Content[i+1]
for depth := 0; val.Kind == yaml.AliasNode && val.Alias != nil && depth < 16; depth++ {
val = val.Alias
}
if key.Kind != yaml.ScalarNode {
return false
}
switch key.Value {
case "format":
seenFormat = val.Kind == yaml.ScalarNode && val.ShortTag() == "!!int"
case "bytes":
seenBytes = val.Kind == yaml.SequenceNode || val.Kind == yaml.ScalarNode && val.ShortTag() == "!!null"
}
}
return seenFormat && seenBytes
}
// encodeGeomVec2s writes a length-prefixed sequence of Vec2 (X, Y
// pairs) for the geometric union (Point as len=1, Lseg/Box as len=2,
// Path/Polygon as len=N). The shared helper keeps the geometric encode
// arms compact and consistent.
func encodeGeomVec2s(enc *gob.Encoder, ps []pgtype.Vec2) error {
if err := enc.Encode(int32(len(ps))); err != nil {
return err
}
for _, p := range ps {
if err := enc.Encode(p.X); err != nil {
return err
}
if err := enc.Encode(p.Y); err != nil {
return err
}
}
return nil
}
// decodeGeomVec2s reads a length-prefixed Vec2 sequence written by
// encodeGeomVec2s. Symmetric inverse.
func decodeGeomVec2s(dec *cellStream) ([]pgtype.Vec2, error) {
var n int32
if err := dec.Decode(&n); err != nil {
return nil, err
}
// Same defensive bounds check as the slice-cell GobDecode arm:
// gob input is untrusted, so a corrupted/hand-edited length
// must not crash make() or drive an unbounded allocation.
if n < 0 {
return nil, fmt.Errorf("decodeGeomVec2s: invalid negative length %d", n)
}
out := make([]pgtype.Vec2, int(n))
for i := 0; i < int(n); i++ {
if err := dec.Decode(&out[i].X); err != nil {
return nil, err
}
if err := dec.Decode(&out[i].Y); err != nil {
return nil, err
}
}
return out, nil
}
// stripBase64Whitespace removes space / tab / CR / LF from s. Used to
// un-fold !!binary payloads the yaml.v3 emitter wraps across multiple
// lines for long scalars; StdEncoding.DecodeString can't tolerate the
// embedded whitespace but the character set we strip is disjoint from
// the base64 alphabet so no legitimate payload byte is lost.
func stripBase64Whitespace(s string) string {
b := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
switch s[i] {
case ' ', '\t', '\r', '\n':
continue
default:
b = append(b, s[i])
}
}
return string(b)
}
// Gob round-trip — the recorder sidecar streams mocks to the agent
// via encoding/gob, and gob cannot round-trip interface{} fields
// without either gob.Register for every concrete value type or a
// custom (GobEncoder, GobDecoder) pair on the enclosing struct. We
// take the second path here: the set of types that can appear in
// Value is dictated by the codec package (int64, float64, string,
// bool, []byte, time.Time, and a small raw-bytes fallback for
// unknown OIDs), and enumerating them with a discriminator byte
// keeps the gob wire format decoupled from any upstream library's
// Register calls. Same shape as MarshalYAML/UnmarshalYAML.
//
// Tag byte assignments (do NOT renumber — mocks on disk and in the
// sidecar stream depend on stability):
//
// 0 → NULL (no payload)
// 1 → int64 (gob int64 — int8 columns)
// 2 → float64 (gob float64)
// 3 → string (gob string)
// 4 → bool (gob bool)
// 5 → []byte (gob []byte)
// 6 → time.Time (gob time.Time — uses time's own GobEncode)
// 7 → rawBytes (format int16 + []byte payload; for unknown OIDs
// represented in this repo as PostgresV3CellRaw.
// We carry just the (format, bytes) tuple and
// GobDecode restores a PostgresV3CellRaw value on
// read if the caller needs the typed form. The
// integrations-side codec consumes that struct via
// RawBytesAndFormat() — there is no conversion
// back to a separate codec RawValue type in this
// package.)
// 8 → int32 (gob int32 — pgtype hands int4 columns back as
// Go int32 when the destination is *any)
// 9 → int16 (gob int16 — same for int2 columns)
// 10 → uint32 (gob uint32 — pgtype's mapping for `oid`,
// `xid`, `cid`. Required for pg_catalog metadata
// queries every JPA / ORM driver runs at boot.)
// 11 → uint16 (gob uint16 — rare but kept for catalogue
// closure)
// 12 → uint64 (gob uint64 — `xid8` on PG ≥ 13)
// 13 → []interface{} (length-prefixed sequence of nested cells —
// PG ARRAY columns: text[], int4[], composite-row,
// multi-dim arrays. Each element is encoded
// recursively through this very GobEncode so the
// type closure stays stable across nesting depth.)
// 14 → map[string]interface{} (length-prefixed sorted (key, nested-
// cell) pairs — PG `json` / `jsonb` columns. Each
// value re-enters GobEncode recursively so nested
// objects / arrays land in the map (this) / slice
// (tag 13) cases. Keys sorted on encode for
// deterministic gob bytes.)
// 15 → int (gob int64 on the wire, decoded back to int —
// yaml.v3 produces Go's untyped int when reloading
// nested integers inside slice / jsonb cells.)
// 16 → float32 (gob float32 — pgtype hands float4 columns back
// as Go float32; distinct from tag 2 float64.)
// 17 → pgtype.Numeric (constituent fields: *big.Int via its own
// GobEncode, Exp int32, InfinityModifier int8, NaN
// bool, Valid bool. Required for PG `numeric` /
// `decimal` columns — listmonk dashboard regression
// surfaced this on /api/settings.)
// 18 → int8 (gob int8 — pgtype.InfinityModifier sentinel for
// ±infinity timestamps, plus literal int8 binds.)
// 19 → pgtype.Interval (Microseconds, Days, Months, Valid.)
// 20 → pgtype.Time (Microseconds, Valid.)
// 21 → pgtype.Bits (Bytes, Len int32, Valid.)
// 22 → geometric union (1-byte sub-discriminator: 1=Point, 2=Line,
// 3=Lseg, 4=Box, 5=Path, 6=Polygon, 7=Circle,
// followed by Vec2 sequence + per-shape extras.)
// 23 → pgtype.TID (BlockNumber uint32, OffsetNumber uint16,
// Valid.)
// 24 → pgtype.TSVector (length + sequence of (Word string, length
// + sequence of (Position uint16, Weight byte)),
// Valid.)
// 25 → netip.Prefix (canonical Prefix.String() form.)
// 26 → net.HardwareAddr (raw bytes, distinct tag from cellTagBytes
// to preserve type identity for pgtype.Encode.)
// 27 → pgtype.Hstore (length + sorted (key, isNil bool, value
// string-when-not-nil) tuples. *string preserves
// the SQL-NULL-inside-hstore semantic.)
// 28 → pgtype.Range[any] (Lower nested-cell bytes, Upper nested-
// cell bytes, LowerType byte, UpperType byte,
// Valid. Bound element type recurses through the
// whole catalogue — int4 → int32, tstzrange →
// time.Time, numrange → pgtype.Numeric, etc.)
// 29 → pgtype.Multirange[Range[any]] (length + sequence of nested
// Range cells through tag 28.)
const (
cellTagNull byte = 0
cellTagInt64 byte = 1
cellTagFloat64 byte = 2
cellTagString byte = 3
cellTagBool byte = 4
cellTagBytes byte = 5
cellTagTime byte = 6
cellTagRaw byte = 7
// Tags 8 and 9 cover the narrower integer types pgtype hands
// back for int4 / int2 columns when the destination is *any.
// Allocated after the original closed set, so older mocks (gob
// streams that only carry tags 0-7) still decode unchanged.
cellTagInt32 byte = 8
cellTagInt16 byte = 9
// Tags 10-12 cover the unsigned-integer types pgtype hands back
// for PG's `oid`, `xid`, `cid`, and `xid8` types. `oid` (uint32)
// is the most common — every pg_catalog metadata query that
// Hibernate / SQLAlchemy / pgjdbc issue at startup returns oid
// columns, and without these tags those mocks fail to gob-encode
// in the recorder sidecar, get dropped on the syncMock channel,
// and the entire app boot wedges on a "no recorded invocation
// matched" against `SELECT … FROM pg_class …`.
cellTagUint32 byte = 10
cellTagUint16 byte = 11
cellTagUint64 byte = 12
// Tag 13 covers PG ARRAY columns: pgtype scans `text[]`, `int4[]`,
// composite-row, etc. into a Go `[]interface{}` whose elements are
// the per-cell logical Go type (string for text[], int32 for int4[],
// nested []interface{} for multi-dim arrays). Encoding strategy:
// length-prefixed sequence of nested PostgresV3Cells, each going
// through this cell's own GobEncode/GobDecode recursively. That
// keeps the type closure stable (no new tag per element type) and
// gracefully nests for multi-dimensional arrays.
cellTagSlice byte = 13
// Tag 14 covers PG JSON / JSONB columns: pgtype scans `jsonb` /
// `json` into a Go `map[string]interface{}` whose values are
// per-key logical types (string for strings, float64 for numbers,
// bool for booleans, nested map[string]interface{} for objects,
// []interface{} for arrays). Encoding strategy: length-prefixed
// sequence of (key, nested-cell) pairs — each value goes through
// PostgresV3Cell.GobEncode recursively, so nested objects + arrays
// re-enter the slice case (tag 13) and the value-type closure stays
// stable. Required for listmonk's `settings` table (its bootstrap
// `SELECT … FROM settings` was the regression case): pre-fix the
// recorder dropped every JSONB-bearing mock and replay couldn't
// satisfy the install path.
cellTagJSONB byte = 14
// Tag 15 covers Go's untyped `int`. Reachable in two cases the
// principal-engineer audit pinned: (1) yaml.v3 decodes nested
// `!!int` values inside slice/jsonb cells into Go's native `int`
// when the destination is `any` (the recursive node.Decode(&v)
// path that handles tag-13 slice / tag-14 jsonb shapes), so a
// fresh-from-disk mock can carry int values inside its array /
// object cells; (2) some app-level helpers wrap counts in plain
// `int` before stuffing them into bind parameters. Width is
// platform-dependent in Go but PG ints are bounded; we narrow to
// int64 on the wire and decode back into int so the round-trip
// is shape-stable.
cellTagInt byte = 15
// Tag 16 covers float4 columns: pgtype hands those back as Go
// `float32` when destination is *any. Distinct from tag 2 which
// is float64 — keeping the source width through gob preserves
// what pgtype.Encode wants on emit (binary float4 is 4 bytes,
// not 8).
cellTagFloat32 byte = 16
// Tag 17 covers `pgtype.Numeric` — PG's arbitrary-precision
// `numeric` / `decimal` type. The struct's fields (Int *big.Int,
// Exp int32, NaN bool, InfinityModifier int8, Valid bool) are
// gob-friendly natively (big.Int has its own GobEncoder). Without
// this tag, listmonk's `GET /api/settings` endpoint dropped its
// recorded mock on every dashboard query that surfaces a numeric
// (counts, rate-limit thresholds, configurable monetary values).
cellTagNumeric byte = 17
// Tag 18 — `int8`. Covers PG's `pgtype.InfinityModifier` (int8 alias)
// returned for `'infinity'` / `'-infinity'` timestamps, plus any
// caller that supplies a literal int8 bind. Kept distinct from
// int16/int32/int64 because gob's reflection won't widen across
// signed integer sizes when the destination is `*any`.
cellTagInt8 byte = 18
// Tag 19 — `pgtype.Interval` (PG `interval`).
cellTagInterval byte = 19
// Tag 20 — `pgtype.Time` (PG `time`).
cellTagPgTime byte = 20
// Tag 21 — `pgtype.Bits` (PG `bit` / `varbit`).
cellTagBits byte = 21
// Tag 22 — geometric union (Point / Line / Lseg / Box / Path /
// Polygon / Circle). One tag with a 1-byte sub-discriminator keeps
// the wire format readable and avoids burning seven tag bytes on
// types that share the same Vec2-based shape.
cellTagGeom byte = 22
// Tag 23 — `pgtype.TID` (PG `tid` / ctid).
cellTagTID byte = 23
// Tag 24 — `pgtype.TSVector` (PG `tsvector`).
cellTagTSVector byte = 24
// Tag 25 — `netip.Prefix` (PG `inet` / `cidr`). Encoded as the
// canonical `Prefix.String()` form so a host-only address (no
// /N) and a network prefix round-trip distinctly.
cellTagNetipPrefix byte = 25
// Tag 26 — `net.HardwareAddr` (PG `macaddr` / `macaddr8`). Stored
// as raw bytes with a distinct tag from cellTagBytes (5) so the
// type identity survives the round-trip — pgtype.Encode's plan
// dispatch picks different paths for `net.HardwareAddr` vs
// `[]byte`, so collapsing them would emit wrong wire bytes.
cellTagMACAddr byte = 26
// Tag 27 — `pgtype.Hstore` (`map[string]*string`). Distinct from
// cellTagJSONB (14) because hstore values are `*string` (with
// nil meaning SQL NULL inside the hstore) while jsonb values
// reach as `interface{}` (with `nil` being JSON null at the
// Go-typing layer too — but the type identity is what
// pgtype.Encode dispatches on).
cellTagHstore byte = 27
// Tag 28 — `pgtype.Range[any]` (PG int4range / int8range /
// numrange / tsrange / tstzrange / daterange). Lower and Upper
// recurse through PostgresV3Cell.GobEncode so the bound element
// type stays per-cohort (int4 → int32, tstzrange → time.Time,
// numrange → pgtype.Numeric, etc.).
cellTagRange byte = 28
// Tag 29 — `pgtype.Multirange[Range[any]]` (PG int4multirange /
// int8multirange / nummultirange / tsmultirange / tstzmultirange
// / datemultirange — added in PG 14). Length-prefixed sequence
// of nested Range cells through tag 28.
cellTagMultirange byte = 29
// Tag 30 — Go `[16]uint8` (the underlying type of `uuid.UUID`).
// pgx's UUIDArrayCodec returns each `uuid[]` element as a raw
// `[16]uint8` fixed-array; without an explicit case the encoder
// falls into the `default:` arm with "unsupported Value type
// [16]uint8" and the mock for that row is dropped. Gob-encoded
// as `[16]uint8` after the tag, same framing as every other
// case in this catalogue (the gob stream carries the
// fixed-array shape on the wire; not a 16-byte raw payload).
cellTagUUIDBytes byte = 30
)
// Geometric sub-discriminators for cellTagGeom. Stable across versions
// — do NOT renumber: existing mocks on disk depend on these.
const (
geomKindPoint byte = 1
geomKindLine byte = 2
geomKindLseg byte = 3
geomKindBox byte = 4
geomKindPath byte = 5
geomKindPolygon byte = 6
geomKindCircle byte = 7
)
// PostgresV3CellRaw is the wire form for a raw-OID fallback cell.
// Mirrors codec.RawValue but lives in pkg/models so the gob path
// doesn't depend on the codec package (which lives in integrations,
// a separate module). Consumers that need a codec.RawValue convert
// at the call site via RawBytesAndFormat() (the codec's encode path
// duck-types on this method to avoid an import cycle back into
// models from codec).
type PostgresV3CellRaw struct {
Format int16
Bytes []byte
}
// RawBytesAndFormat exposes the (format, bytes) tuple without
// requiring codec to import models. Returned bytes alias the struct
// — callers must not retain a reference past the cell's lifetime.
func (r PostgresV3CellRaw) RawBytesAndFormat() (int16, []byte) {
return r.Format, r.Bytes
}
// GobEncode writes the cell to a gob stream using the tag-byte
// format documented above. Equivalent of MarshalYAML for the gob
// transport path (sidecar → agent).
func (c PostgresV3Cell) GobEncode() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
switch v := c.Value.(type) {
case nil:
if err := buf.WriteByte(cellTagNull); err != nil {
return nil, err
}
case int64:
buf.WriteByte(cellTagInt64)
if err := enc.Encode(v); err != nil {
return nil, err
}
case int32:
// pgtype scans int4 into a Go int32 when the destination is
// `*any`, so this is the on-wire shape for every smallint/
// integer column the recorder captures. We preserve the
// concrete type rather than widening to int64 because the
// codec's encode path matches against the source type when
// re-emitting and an int64 ↔ int4 mismatch reads as a
// driver-level type tag drift on replay.
buf.WriteByte(cellTagInt32)
if err := enc.Encode(v); err != nil {
return nil, err
}
case int16:
// Same rationale as int32 — pgtype maps int2 to Go int16.
buf.WriteByte(cellTagInt16)
if err := enc.Encode(v); err != nil {
return nil, err
}
case uint32:
// PostgreSQL's `oid`, `xid`, and `cid` types all surface as
// Go uint32 from pgtype.Map.Scan — and they show up
// constantly in the pg_catalog metadata queries every JPA /
// ORM driver runs at boot (Hibernate's
// `SELECT n.nspname,c.relname,a.attname,a.atttypid,...
// FROM pg_class c JOIN pg_attribute a ON …` is the load-
// bearing one). Drop a tag here so those mocks gob-encode
// successfully and the recorder doesn't lose the entire
// boot-time mock cohort.
buf.WriteByte(cellTagUint32)
if err := enc.Encode(v); err != nil {
return nil, err
}
case uint16:
// pgtype emits uint16 for some attlen / typmod-aligned
// columns; rare but keeping the catalogue closed is
// cheap.
buf.WriteByte(cellTagUint16)
if err := enc.Encode(v); err != nil {
return nil, err
}
case uint64:
// xid8 (PG 13+ 64-bit transaction ids) — pgtype maps it to
// Go uint64. Required for any txid_current() or advisory-
// lock workload running on a recent PG.
buf.WriteByte(cellTagUint64)
if err := enc.Encode(v); err != nil {
return nil, err
}
case int:
// Reachable when a slice / jsonb cell is reloaded from
// mocks.yaml: yaml.v3 decodes nested `!!int` into Go's
// platform-dependent `int` whenever the destination is
// `any`, which is the recursive node.Decode(&v) path
// inside this cell's slice (tag 13) and jsonb (tag 14)
// branches. Without this case, GobEncode would fail-loud
// on the rebuilt cell and the storemocks step would abort
// the entire replay. Width is platform-dependent in Go but
// the wire form is fixed at int64 — we don't lose range on
// either 32- or 64-bit platforms because PG ints are
// bounded.
buf.WriteByte(cellTagInt)
if err := enc.Encode(int64(v)); err != nil {
return nil, err
}
case float32:
// pgtype hands float4 columns back as Go float32. Distinct
// from cellTagFloat64 (tag 2) so the source width survives
// on emit — the binary float4 wire format is 4 bytes, not 8,
// and pgtype.Encode for OIDFloat4 expects float32.
buf.WriteByte(cellTagFloat32)
if err := enc.Encode(v); err != nil {
return nil, err
}
case pgtype.Numeric:
// PG `numeric` / `decimal` — arbitrary-precision. Encode the
// constituent fields directly: *big.Int via its own GobEncode
// (handles nil → empty-bytes), Exp / InfinityModifier / NaN /
// Valid via gob's primitive emit. We canonicalise nil Int to
// big.NewInt(0) so the gob stream is deterministic for the
// "valid Numeric with zero/null payload" case (rare but
// reachable on degenerate inputs). Decode reconstructs the
// struct verbatim so pgtype.Encode on emit gets the exact
// shape it expects.
buf.WriteByte(cellTagNumeric)
// Encode an Int-presence flag separately from the bytes:
// big.Int.GobEncode() of a fresh big.NewInt(0) produces a
// non-empty payload that's indistinguishable from "absent
// Int" if we encoded nil → zero-int. NaN / ±infinity numerics
// legitimately carry Int=nil and the round-trip must preserve
// that nil-ness so pgtype.Encode picks the right wire shape
// on emit. Tag the presence explicitly.
hasInt := v.Int != nil
if err := enc.Encode(hasInt); err != nil {
return nil, err
}
if hasInt {
intBytes, err := v.Int.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode numeric Int: %w", err)
}
if err := enc.Encode(intBytes); err != nil {
return nil, err
}
}
if err := enc.Encode(v.Exp); err != nil {
return nil, err
}
if err := enc.Encode(int8(v.InfinityModifier)); err != nil {
return nil, err
}
if err := enc.Encode(v.NaN); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case int8:
buf.WriteByte(cellTagInt8)
if err := enc.Encode(v); err != nil {
return nil, err
}
case pgtype.Interval:
buf.WriteByte(cellTagInterval)
if err := enc.Encode(v.Microseconds); err != nil {
return nil, err
}
if err := enc.Encode(v.Days); err != nil {
return nil, err
}
if err := enc.Encode(v.Months); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Time:
buf.WriteByte(cellTagPgTime)
if err := enc.Encode(v.Microseconds); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Bits:
buf.WriteByte(cellTagBits)
if err := enc.Encode(v.Bytes); err != nil {
return nil, err
}
if err := enc.Encode(v.Len); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Point:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindPoint); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, []pgtype.Vec2{v.P}); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Line:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindLine); err != nil {
return nil, err
}
// A·x + B·y + C = 0 — three coefficients, no Vec2.
if err := enc.Encode([3]float64{v.A, v.B, v.C}); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Lseg:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindLseg); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, v.P[:]); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Box:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindBox); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, v.P[:]); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Path:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindPath); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, v.P); err != nil {
return nil, err
}
if err := enc.Encode(v.Closed); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Polygon:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindPolygon); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, v.P); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Circle:
buf.WriteByte(cellTagGeom)
if err := enc.Encode(geomKindCircle); err != nil {
return nil, err
}
if err := encodeGeomVec2s(enc, []pgtype.Vec2{v.P}); err != nil {
return nil, err
}
if err := enc.Encode(v.R); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.TID:
buf.WriteByte(cellTagTID)
if err := enc.Encode(v.BlockNumber); err != nil {
return nil, err
}
if err := enc.Encode(v.OffsetNumber); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.TSVector:
buf.WriteByte(cellTagTSVector)
if err := enc.Encode(int32(len(v.Lexemes))); err != nil {
return nil, err
}
for i, lex := range v.Lexemes {
if err := enc.Encode(lex.Word); err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode tsvector lex %d word: %w", i, err)
}
if err := enc.Encode(int32(len(lex.Positions))); err != nil {
return nil, err
}
for _, p := range lex.Positions {
if err := enc.Encode(p.Position); err != nil {
return nil, err
}
if err := enc.Encode(byte(p.Weight)); err != nil {
return nil, err
}
}
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case netip.Prefix:
buf.WriteByte(cellTagNetipPrefix)
// Canonical string form: a host-only IPv4 reads as "1.2.3.4/32"
// and a /24 network prefix reads as "1.2.3.0/24" — distinct,
// round-trip clean via netip.ParsePrefix on decode.
if err := enc.Encode(v.String()); err != nil {
return nil, err
}
case net.HardwareAddr:
buf.WriteByte(cellTagMACAddr)
// Stored as raw bytes; the distinct tag preserves type
// identity for pgtype.Encode's plan dispatch.
if err := enc.Encode([]byte(v)); err != nil {
return nil, err
}
case pgtype.Hstore:
buf.WriteByte(cellTagHstore)
if err := enc.Encode(int32(len(v))); err != nil {
return nil, err
}
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if err := enc.Encode(k); err != nil {
return nil, err
}
val := v[k]
isNil := val == nil
if err := enc.Encode(isNil); err != nil {
return nil, err
}
if !isNil {
if err := enc.Encode(*val); err != nil {
return nil, err
}
}
}
case pgtype.Range[any]:
buf.WriteByte(cellTagRange)
// Bounds recurse through this very GobEncode so the bound
// element type (int32 for int4range, time.Time for tstzrange,
// pgtype.Numeric for numrange, etc.) is preserved verbatim.
lower, err := PostgresV3Cell{Value: v.Lower}.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode range lower (%T): %w", v.Lower, err)
}
upper, err := PostgresV3Cell{Value: v.Upper}.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode range upper (%T): %w", v.Upper, err)
}
if err := enc.Encode(lower); err != nil {
return nil, err
}
if err := enc.Encode(upper); err != nil {
return nil, err
}
if err := enc.Encode(byte(v.LowerType)); err != nil {
return nil, err
}
if err := enc.Encode(byte(v.UpperType)); err != nil {
return nil, err
}
if err := enc.Encode(v.Valid); err != nil {
return nil, err
}
case pgtype.Multirange[pgtype.Range[any]]:
buf.WriteByte(cellTagMultirange)
if err := enc.Encode(int32(len(v))); err != nil {
return nil, err
}
for i, r := range v {
rangeBytes, err := PostgresV3Cell{Value: r}.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode multirange[%d]: %w", i, err)
}
if err := enc.Encode(rangeBytes); err != nil {
return nil, err
}
}
case map[string]interface{}:
// PG `json` / `jsonb` columns: pgtype.Map.Scan resolves these
// into a Go `map[string]interface{}` whose values are the per-
// key logical Go type (string, float64, bool, nested
// map[string]interface{}, []interface{} for arrays). Encode
// strategy: length-prefix + sorted (key, nested-cell) pairs
// — keys sorted so the gob byte stream is deterministic for a
// given map regardless of Go's randomized map-iteration order.
// Each value re-enters GobEncode recursively so nested objects
// + arrays land in the slice (tag 13) / map (this) cases.
// Without this case listmonk's bootstrap `SELECT … FROM
// settings` (settings.value is jsonb) dropped its mock and
// the install-time replay served 0-arg ParseComplete to a
// 7-arg INSERT INTO lists, crashing the app.
buf.WriteByte(cellTagJSONB)
if err := enc.Encode(int32(len(v))); err != nil {
return nil, err
}
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if err := enc.Encode(k); err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode jsonb key %q: %w", k, err)
}
elemBytes, err := PostgresV3Cell{Value: v[k]}.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode jsonb value at %q (%T): %w", k, v[k], err)
}
if err := enc.Encode(elemBytes); err != nil {
return nil, err
}
}
case []interface{}:
// PG ARRAY columns: pgtype.Map.Scan resolves text[] / int4[] /
// composite-row / multi-dim arrays into a `[]interface{}` whose
// elements are the per-cell logical Go type. Encode as a length-
// prefixed sequence of nested cells so element types stay
// per-position and multi-dim nesting works recursively. Without
// this case, listmonk's install-time INSERT INTO lists (...,
// tags TEXT[], ...) recorded a no-PD mock (because the gob
// failure dropped the entire mock including its
// ParameterDescription) and replay served a 0-arg ParseComplete
// to a 7-arg INSERT — pgx aborted with "expected 0 arguments,
// got 7" and listmonk crashed before any user traffic.
buf.WriteByte(cellTagSlice)
if err := enc.Encode(int32(len(v))); err != nil {
return nil, err
}
for i, elem := range v {
elemBytes, err := PostgresV3Cell{Value: elem}.GobEncode()
if err != nil {
return nil, fmt.Errorf("PostgresV3Cell.GobEncode slice elem %d (%T): %w", i, elem, err)
}
if err := enc.Encode(elemBytes); err != nil {
return nil, err
}
}
case float64:
buf.WriteByte(cellTagFloat64)
if err := enc.Encode(v); err != nil {
return nil, err
}
case string:
buf.WriteByte(cellTagString)
if err := enc.Encode(v); err != nil {
return nil, err
}
case bool:
buf.WriteByte(cellTagBool)
if err := enc.Encode(v); err != nil {
return nil, err
}
case []byte:
buf.WriteByte(cellTagBytes)
if err := enc.Encode(v); err != nil {
return nil, err
}
case time.Time:
buf.WriteByte(cellTagTime)
if err := enc.Encode(v); err != nil {
return nil, err
}
case PostgresV3CellRaw:
buf.WriteByte(cellTagRaw)
if err := enc.Encode(v); err != nil {
return nil, err
}
case *PostgresV3CellRaw:
// A typed-nil pointer survives the `case nil:` arm above
// because (*PostgresV3CellRaw)(nil) has a non-nil type
// descriptor; without this guard `enc.Encode(*v)` would
// panic on the nil dereference. Treat it the same as a
// SQL NULL — the call site clearly intended an absent
// value, and writing tagRaw with a zero RawValue would
// emit garbled bytes on replay.
if v == nil {
buf.WriteByte(cellTagNull)
break
}
buf.WriteByte(cellTagRaw)
if err := enc.Encode(*v); err != nil {
return nil, err
}
case [16]uint8:
// pgx's UUIDArrayCodec returns each `uuid[]` element as a raw
// `[16]uint8` and the bare `uuid` column scan path lands here
// too when callers Scan into `*[16]byte` instead of pgtype.UUID.
// Gob-encode the [16]uint8 after the tag (same framing as
// every other case in this switch — gob carries the
// fixed-array shape on the wire) so GobDecode can reconstruct
// it symmetrically.
buf.WriteByte(cellTagUUIDBytes)
if err := enc.Encode(v); err != nil {
return nil, err
}
default:
// Fail loud rather than silently downgrade. An earlier revision
// of this branch routed unknown types through fmt.Sprint and
// stored the result as a string cell, but that is silently
// lossy: pgtype.Numeric, pgtype.Interval, array decodes, etc.
// all stringify to forms the codec cannot re-encode under the
// original OID, so the cell would emit zero bytes on replay
// and pgjdbc would crash in ByteConverter with an
// ArrayIndexOutOfBoundsException — the kind of failure that
// surfaces deep in the test set with no obvious link back to
// the recorder. Surfacing the type at record time gives an
// actionable signal: the missing case is named, the OID can
// be added to the codec catalogue, and a tag byte can be
// allocated in this switch.
return nil, fmt.Errorf("PostgresV3Cell.GobEncode: unsupported Value type %T — add a tag byte and an explicit case to GobEncode/GobDecode (and a codec entry in integrations/pkg/postgres/v3/codec) for new pgtype return types", v)
}
return buf.Bytes(), nil
}
// GobDecode restores the cell from the gob stream produced by
// GobEncode. Symmetric dispatch on the tag byte.
func (c *PostgresV3Cell) GobDecode(data []byte) error {
if len(data) == 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode: empty buffer")
}
tag := data[0]
// Read as a gob decoder over data[1:] reads it, with no decoder built
// for a value gob needs no type definition for (cellStream).
dec := &cellStream{b: data[1:]}
switch tag {
case cellTagNull:
c.Value = nil
case cellTagInt64:
var v int64
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode int64: %w", err)
}
c.Value = v
case cellTagFloat64:
var v float64
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode float64: %w", err)
}
c.Value = v
case cellTagString:
var v string
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode string: %w", err)
}
c.Value = v
case cellTagBool:
var v bool
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode bool: %w", err)
}
c.Value = v
case cellTagBytes:
var v []byte
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode bytes: %w", err)
}
c.Value = v
case cellTagTime:
var v time.Time
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode time: %w", err)
}
c.Value = v
case cellTagRaw:
var v PostgresV3CellRaw
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode raw: %w", err)
}
c.Value = v
case cellTagInt32:
var v int32
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode int32: %w", err)
}
c.Value = v
case cellTagInt16:
var v int16
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode int16: %w", err)
}
c.Value = v
case cellTagUint32:
var v uint32
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode uint32: %w", err)
}
c.Value = v
case cellTagUint16:
var v uint16
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode uint16: %w", err)
}
c.Value = v
case cellTagUint64:
var v uint64
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode uint64: %w", err)
}
c.Value = v
case cellTagInt:
var v int64
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode int: %w", err)
}
// Decode back into Go's untyped int — the round-trip target
// is the same shape yaml.v3 produced on load. int64 → int
// narrows on 32-bit platforms but PG int values fit; bigger
// numerics live in pgtype.Numeric (tag 17).
c.Value = int(v)
case cellTagFloat32:
var v float32
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode float32: %w", err)
}
c.Value = v
case cellTagNumeric:
var (
hasInt bool
expVal int32
infMod int8
nanFlag bool
validFlag bool
)
if err := dec.Decode(&hasInt); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric hasInt flag: %w", err)
}
recovered := pgtype.Numeric{}
if hasInt {
var intBytes []byte
if err := dec.Decode(&intBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric Int bytes: %w", err)
}
recovered.Int = new(big.Int)
if err := recovered.Int.GobDecode(intBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric Int gob: %w", err)
}
}
if err := dec.Decode(&expVal); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric Exp: %w", err)
}
if err := dec.Decode(&infMod); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric InfinityModifier: %w", err)
}
if err := dec.Decode(&nanFlag); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric NaN: %w", err)
}
if err := dec.Decode(&validFlag); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode numeric Valid: %w", err)
}
recovered.Exp = expVal
recovered.InfinityModifier = pgtype.InfinityModifier(infMod)
recovered.NaN = nanFlag
recovered.Valid = validFlag
c.Value = recovered
case cellTagInt8:
var v int8
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode int8: %w", err)
}
c.Value = v
case cellTagInterval:
var ivl pgtype.Interval
if err := dec.Decode(&ivl.Microseconds); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode interval Microseconds: %w", err)
}
if err := dec.Decode(&ivl.Days); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode interval Days: %w", err)
}
if err := dec.Decode(&ivl.Months); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode interval Months: %w", err)
}
if err := dec.Decode(&ivl.Valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode interval Valid: %w", err)
}
c.Value = ivl
case cellTagPgTime:
var tm pgtype.Time
if err := dec.Decode(&tm.Microseconds); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode pgtype.Time Microseconds: %w", err)
}
if err := dec.Decode(&tm.Valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode pgtype.Time Valid: %w", err)
}
c.Value = tm
case cellTagBits:
var b pgtype.Bits
if err := dec.Decode(&b.Bytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode bits Bytes: %w", err)
}
if err := dec.Decode(&b.Len); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode bits Len: %w", err)
}
if err := dec.Decode(&b.Valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode bits Valid: %w", err)
}
c.Value = b
case cellTagGeom:
var kind byte
if err := dec.Decode(&kind); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode geom discriminator: %w", err)
}
switch kind {
case geomKindPoint:
ps, err := decodeGeomVec2s(dec)
if err != nil || len(ps) != 1 {
return fmt.Errorf("PostgresV3Cell.GobDecode point Vec2: %v (len=%d)", err, len(ps))
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode point Valid: %w", err)
}
c.Value = pgtype.Point{P: ps[0], Valid: valid}
case geomKindLine:
var coef [3]float64
if err := dec.Decode(&coef); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode line coef: %w", err)
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode line Valid: %w", err)
}
c.Value = pgtype.Line{A: coef[0], B: coef[1], C: coef[2], Valid: valid}
case geomKindLseg:
ps, err := decodeGeomVec2s(dec)
if err != nil || len(ps) != 2 {
return fmt.Errorf("PostgresV3Cell.GobDecode lseg Vec2: %v (len=%d)", err, len(ps))
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode lseg Valid: %w", err)
}
c.Value = pgtype.Lseg{P: [2]pgtype.Vec2{ps[0], ps[1]}, Valid: valid}
case geomKindBox:
ps, err := decodeGeomVec2s(dec)
if err != nil || len(ps) != 2 {
return fmt.Errorf("PostgresV3Cell.GobDecode box Vec2: %v (len=%d)", err, len(ps))
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode box Valid: %w", err)
}
c.Value = pgtype.Box{P: [2]pgtype.Vec2{ps[0], ps[1]}, Valid: valid}
case geomKindPath:
ps, err := decodeGeomVec2s(dec)
if err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode path Vec2s: %w", err)
}
var closed, valid bool
if err := dec.Decode(&closed); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode path Closed: %w", err)
}
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode path Valid: %w", err)
}
c.Value = pgtype.Path{P: ps, Closed: closed, Valid: valid}
case geomKindPolygon:
ps, err := decodeGeomVec2s(dec)
if err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode polygon Vec2s: %w", err)
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode polygon Valid: %w", err)
}
c.Value = pgtype.Polygon{P: ps, Valid: valid}
case geomKindCircle:
ps, err := decodeGeomVec2s(dec)
if err != nil || len(ps) != 1 {
return fmt.Errorf("PostgresV3Cell.GobDecode circle Vec2: %v (len=%d)", err, len(ps))
}
var r float64
if err := dec.Decode(&r); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode circle R: %w", err)
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode circle Valid: %w", err)
}
c.Value = pgtype.Circle{P: ps[0], R: r, Valid: valid}
default:
return fmt.Errorf("PostgresV3Cell.GobDecode geom: unknown kind %d", kind)
}
case cellTagTID:
var tid pgtype.TID
if err := dec.Decode(&tid.BlockNumber); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tid BlockNumber: %w", err)
}
if err := dec.Decode(&tid.OffsetNumber); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tid OffsetNumber: %w", err)
}
if err := dec.Decode(&tid.Valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tid Valid: %w", err)
}
c.Value = tid
case cellTagTSVector:
var n int32
if err := dec.Decode(&n); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector length: %w", err)
}
// Bounds-check before allocating: gob input is untrusted (see
// the cellTagSlice arm below for the full threat-model note).
// A negative or absurd length must not drive a runtime panic
// in make() or an unbounded allocation. Symmetric guard.
if n < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector length: invalid negative length %d", n)
}
lexemes := make([]pgtype.TSVectorLexeme, n)
for i := int32(0); i < n; i++ {
if err := dec.Decode(&lexemes[i].Word); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector lex %d word: %w", i, err)
}
var pn int32
if err := dec.Decode(&pn); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector lex %d pos count: %w", i, err)
}
if pn < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector lex %d pos count: invalid negative length %d", i, pn)
}
poss := make([]pgtype.TSVectorPosition, pn)
for j := int32(0); j < pn; j++ {
if err := dec.Decode(&poss[j].Position); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector lex %d pos %d Position: %w", i, j, err)
}
var w byte
if err := dec.Decode(&w); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector lex %d pos %d Weight: %w", i, j, err)
}
poss[j].Weight = pgtype.TSVectorWeight(w)
}
lexemes[i].Positions = poss
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode tsvector Valid: %w", err)
}
c.Value = pgtype.TSVector{Lexemes: lexemes, Valid: valid}
case cellTagNetipPrefix:
var s string
if err := dec.Decode(&s); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode netip.Prefix: %w", err)
}
p, err := netip.ParsePrefix(s)
if err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode netip.Prefix parse %q: %w", s, err)
}
c.Value = p
case cellTagMACAddr:
var b []byte
if err := dec.Decode(&b); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode net.HardwareAddr: %w", err)
}
c.Value = net.HardwareAddr(b)
case cellTagHstore:
var n int32
if err := dec.Decode(&n); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode hstore length: %w", err)
}
// Bounds-check before allocating: gob input is untrusted (see
// the cellTagSlice arm below for the full threat-model note).
if n < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode hstore length: invalid negative length %d", n)
}
out := make(pgtype.Hstore, n)
for i := int32(0); i < n; i++ {
var k string
if err := dec.Decode(&k); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode hstore key %d: %w", i, err)
}
var isNil bool
if err := dec.Decode(&isNil); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode hstore nil-flag for %q: %w", k, err)
}
if isNil {
out[k] = nil
continue
}
var val string
if err := dec.Decode(&val); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode hstore value for %q: %w", k, err)
}
out[k] = &val
}
c.Value = out
case cellTagRange:
var lowerBytes, upperBytes []byte
if err := dec.Decode(&lowerBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range lower bytes: %w", err)
}
if err := dec.Decode(&upperBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range upper bytes: %w", err)
}
var lt, ut byte
if err := dec.Decode(<); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range LowerType: %w", err)
}
if err := dec.Decode(&ut); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range UpperType: %w", err)
}
var valid bool
if err := dec.Decode(&valid); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range Valid: %w", err)
}
var lower, upper PostgresV3Cell
if err := lower.GobDecode(lowerBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range lower cell: %w", err)
}
if err := upper.GobDecode(upperBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode range upper cell: %w", err)
}
c.Value = pgtype.Range[any]{
Lower: lower.Value,
Upper: upper.Value,
LowerType: pgtype.BoundType(lt),
UpperType: pgtype.BoundType(ut),
Valid: valid,
}
case cellTagMultirange:
var n int32
if err := dec.Decode(&n); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode multirange length: %w", err)
}
// Bounds-check before allocating: gob input is untrusted (see
// the cellTagSlice arm below for the full threat-model note).
if n < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode multirange length: invalid negative length %d", n)
}
out := make(pgtype.Multirange[pgtype.Range[any]], n)
for i := int32(0); i < n; i++ {
var rangeBytes []byte
if err := dec.Decode(&rangeBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode multirange[%d] bytes: %w", i, err)
}
var rcell PostgresV3Cell
if err := rcell.GobDecode(rangeBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode multirange[%d] cell: %w", i, err)
}
r, ok := rcell.Value.(pgtype.Range[any])
if !ok {
return fmt.Errorf("PostgresV3Cell.GobDecode multirange[%d]: nested cell is %T, want pgtype.Range[any]", i, rcell.Value)
}
out[i] = r
}
c.Value = out
case cellTagJSONB:
// Symmetric to the GobEncode map case: read length, then
// (key, nested-cell-bytes) pairs, decode each value through
// PostgresV3Cell.GobDecode recursively. Map iteration on
// the encode side is sorted so the gob stream is
// deterministic; rebuild does not need to preserve order
// (Go's map type is unordered).
var n int32
if err := dec.Decode(&n); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode jsonb length: %w", err)
}
// Bounds-check before allocating: gob input is untrusted (see
// the cellTagSlice arm below for the full threat-model note).
// A negative length on a map make() doesn't itself panic, but
// we still reject it eagerly so the loop's `for i < n` doesn't
// go negative-iterations and surface the corruption later as
// a confusing nested decode error.
if n < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode jsonb length: invalid negative length %d", n)
}
out := make(map[string]interface{}, n)
for i := int32(0); i < n; i++ {
var k string
if err := dec.Decode(&k); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode jsonb key %d: %w", i, err)
}
var elemBytes []byte
if err := dec.Decode(&elemBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode jsonb value bytes for %q: %w", k, err)
}
var elem PostgresV3Cell
if err := elem.GobDecode(elemBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode jsonb value cell for %q: %w", k, err)
}
out[k] = elem.Value
}
c.Value = out
case cellTagSlice:
// Symmetric to the GobEncode []interface{} branch: read the
// length, then each element as a self-contained nested cell
// gob blob, and decode it back to its underlying Value. The
// rebuild produces a fresh []interface{} the caller can
// type-assert per its own per-OID expectations (text[]
// elements come back as string, int4[] as int32, multi-dim
// arrays as nested []interface{} via the recursive case).
var n int32
if err := dec.Decode(&n); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode slice length: %w", err)
}
// Bounds-check before allocating: gob input is untrusted (it
// arrives over the recorder→agent socket and from on-disk
// mocks that may have been hand-edited), so a malformed
// length must not be allowed to drive a 2-GiB allocation or
// a panic via make's runtime length check.
if n < 0 {
return fmt.Errorf("PostgresV3Cell.GobDecode slice length: invalid negative length %d", n)
}
out := make([]interface{}, int(n))
for i := 0; i < int(n); i++ {
var elemBytes []byte
if err := dec.Decode(&elemBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode slice elem %d bytes: %w", i, err)
}
var elem PostgresV3Cell
if err := elem.GobDecode(elemBytes); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode slice elem %d cell: %w", i, err)
}
out[i] = elem.Value
}
c.Value = out
case cellTagUUIDBytes:
// Symmetric to the [16]uint8 GobEncode case: gob-decode the
// fixed-array shape pgx originally produced. We decode into a
// [16]uint8 (not uuid.UUID) so the models package stays free
// of a uuid dependency; callers that want uuid.UUID can
// convert with `uuid.UUID(arr)`.
var arr [16]uint8
if err := dec.Decode(&arr); err != nil {
return fmt.Errorf("PostgresV3Cell.GobDecode uuidBytes: %w", err)
}
c.Value = arr
default:
return fmt.Errorf("PostgresV3Cell.GobDecode: unknown tag %d", tag)
}
return nil
}
// PostgresV3Cells is a row (sequence of cells). Named type so
// sequence-element dispatch goes through our UnmarshalYAML — yaml.v3's
// default sequence-of-nil short-circuit otherwise bypasses
// UnmarshalYAML for elements whose YAML representation is null.
type PostgresV3Cells []PostgresV3Cell
// UnmarshalYAML walks the sequence node by hand so every element's
// UnmarshalYAML is invoked (including null-valued scalars that
// yaml.v3 would otherwise short-circuit).
func (cs *PostgresV3Cells) UnmarshalYAML(node *yaml.Node) error {
// node==nil is a distinct failure mode from "wrong kind": the
// previous combined guard would dereference node.Kind in the
// error formatter and panic. Split them so a missing/empty
// node surfaces a descriptive error instead of crashing the
// loader on hand-edited YAML.
if node == nil {
return fmt.Errorf("PostgresV3Cells: expected sequence, got nil node")
}
if node.Kind != yaml.SequenceNode {
return fmt.Errorf("PostgresV3Cells: expected sequence, got kind=%d", node.Kind)
}
out := make(PostgresV3Cells, len(node.Content))
for i, child := range node.Content {
if err := (&out[i]).UnmarshalYAML(child); err != nil {
return fmt.Errorf("PostgresV3Cells[%d]: %w", i, err)
}
}
*cs = out
return nil
}