snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
208
internal/pgmapbroker/pgx_helpers.go
Normal file
208
internal/pgmapbroker/pgx_helpers.go
Normal file
@@ -0,0 +1,208 @@
|
||||
//go:build !appengine
|
||||
|
||||
package pgmapbroker
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/convert"
|
||||
)
|
||||
|
||||
// byteArena is a chunked arena allocator for byte data. It copies incoming
|
||||
// slices into a contiguous buffer and hands back sub-slices (or strings via
|
||||
// unsafe conversion). When the current chunk is full a new one is allocated;
|
||||
// old chunks stay alive via the slices/strings already referencing them.
|
||||
type byteArena struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// copyBytes copies src into the arena and returns a sub-slice of the arena buffer.
|
||||
// The returned slice has cap==len (three-index slice) so that append on it
|
||||
// cannot silently overwrite adjacent arena data.
|
||||
func (a *byteArena) copyBytes(src []byte) []byte {
|
||||
n := len(src)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
if cap(a.buf)-len(a.buf) < n {
|
||||
newCap := 2 * cap(a.buf)
|
||||
if newCap < n {
|
||||
newCap = n
|
||||
}
|
||||
if newCap < 4096 {
|
||||
newCap = 4096
|
||||
}
|
||||
a.buf = make([]byte, 0, newCap)
|
||||
}
|
||||
start := len(a.buf)
|
||||
a.buf = append(a.buf, src...)
|
||||
return a.buf[start : start+n : start+n]
|
||||
}
|
||||
|
||||
// copyString copies src into the arena and returns a string backed by arena memory.
|
||||
func (a *byteArena) copyString(src []byte) string {
|
||||
if len(src) == 0 {
|
||||
return ""
|
||||
}
|
||||
b := a.copyBytes(src)
|
||||
return convert.BytesToString(b)
|
||||
}
|
||||
|
||||
// pgTextFormat is the pgx text wire format code.
|
||||
const pgTextFormat int16 = 0
|
||||
|
||||
// pgRawInt64 parses a pgx int8 value as int64.
|
||||
// Handles both binary wire format (8-byte big-endian) and text (decimal string).
|
||||
// Returns 0 for nil input.
|
||||
func pgRawInt64(b []byte, format int16) int64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
if format == pgTextFormat {
|
||||
v, _ := strconv.ParseInt(convert.BytesToString(b), 10, 64)
|
||||
return v
|
||||
}
|
||||
return int64(binary.BigEndian.Uint64(b))
|
||||
}
|
||||
|
||||
// pgRawUint64 parses a pgx int8 value as uint64.
|
||||
// Handles both binary wire format (8-byte big-endian) and text (decimal string).
|
||||
// Returns 0 for nil input.
|
||||
func pgRawUint64(b []byte, format int16) uint64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
if format == pgTextFormat {
|
||||
v, _ := strconv.ParseUint(convert.BytesToString(b), 10, 64)
|
||||
return v
|
||||
}
|
||||
return binary.BigEndian.Uint64(b)
|
||||
}
|
||||
|
||||
// pgRawInt32 parses a pgx int4 value as int32.
|
||||
// Handles both binary wire format (4-byte big-endian) and text (decimal string).
|
||||
// Returns 0 for nil input.
|
||||
func pgRawInt32(b []byte, format int16) int32 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
if format == pgTextFormat {
|
||||
v, _ := strconv.ParseInt(convert.BytesToString(b), 10, 32)
|
||||
return int32(v)
|
||||
}
|
||||
return int32(binary.BigEndian.Uint32(b))
|
||||
}
|
||||
|
||||
// pgRawBool parses a pgx boolean value.
|
||||
// Handles both binary wire format (0x00/0x01) and text ("t"/"f").
|
||||
// Returns false for nil input.
|
||||
func pgRawBool(b []byte, format int16) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
if format == pgTextFormat {
|
||||
return len(b) > 0 && b[0] == 't'
|
||||
}
|
||||
return b[0] == 1
|
||||
}
|
||||
|
||||
// pgRawString copies a pgx text value into the arena and returns the resulting
|
||||
// string. TEXT/VARCHAR have identical representation in both binary and text wire
|
||||
// formats, so no format parameter is needed. Returns "" for nil input.
|
||||
func pgRawString(a *byteArena, b []byte) string {
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return a.copyString(b)
|
||||
}
|
||||
|
||||
// pgRawBytes copies a pgx BYTEA value into the arena.
|
||||
// Handles both binary wire format (raw bytes) and text (hex-encoded \xDEAD...).
|
||||
// Returns nil for nil input.
|
||||
func pgRawBytes(a *byteArena, b []byte, format int16) []byte {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if format == pgTextFormat && len(b) >= 2 && b[0] == '\\' && b[1] == 'x' {
|
||||
decoded := make([]byte, hex.DecodedLen(len(b)-2))
|
||||
n, err := hex.Decode(decoded, b[2:])
|
||||
if err != nil {
|
||||
return a.copyBytes(b) // fallback: copy as-is
|
||||
}
|
||||
return a.copyBytes(decoded[:n])
|
||||
}
|
||||
return a.copyBytes(b)
|
||||
}
|
||||
|
||||
// pgRawJSONBBytes extracts the JSON body from a pgx JSONB value into the arena.
|
||||
// Binary wire format has a 1-byte version header that must be stripped.
|
||||
// Text wire format is plain JSON (no header). Returns nil for nil input.
|
||||
func pgRawJSONBBytes(a *byteArena, b []byte, format int16) []byte {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if format != pgTextFormat && len(b) > 1 {
|
||||
return a.copyBytes(b[1:]) // strip version byte
|
||||
}
|
||||
return a.copyBytes(b)
|
||||
}
|
||||
|
||||
// pgColFormats holds per-column wire format codes extracted from pgx rows.
|
||||
// Format values: 0 = text, 1 = binary. Call pgColFormatsFromRows once per
|
||||
// query result, before iterating rows.
|
||||
type pgColFormats []int16
|
||||
|
||||
// pgEpochDeltaUsec is the difference in microseconds between the Unix epoch
|
||||
// (1970-01-01) and the PostgreSQL epoch (2000-01-01).
|
||||
const pgEpochDeltaUsec = 946684800_000_000
|
||||
|
||||
// pgRawTimestampMillis parses a pgx TIMESTAMPTZ value as Unix milliseconds.
|
||||
// Binary wire format: 8-byte big-endian int64 = microseconds since PG epoch (2000-01-01 UTC).
|
||||
// Text wire format: parsed with time.Parse. Returns 0 for nil input.
|
||||
func pgRawTimestampMillis(b []byte, format int16) int64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
if format == pgTextFormat {
|
||||
// PostgreSQL renders timestamptz with a variable-width zone offset - hour
|
||||
// only ("+00"), hour:minute ("+05:30"), or the rare hour:minute:second - so
|
||||
// no single Go layout matches all of them (the previous "Z07:00:00" layout
|
||||
// parsed none of PostgreSQL's actual output, zeroing every timestamp under
|
||||
// the simple-query protocol). Try the three offset widths.
|
||||
s := convert.BytesToString(b)
|
||||
for _, layout := range []string{
|
||||
"2006-01-02 15:04:05.999999-07",
|
||||
"2006-01-02 15:04:05.999999-07:00",
|
||||
"2006-01-02 15:04:05.999999-07:00:00",
|
||||
} {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t.UnixMilli()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
usec := int64(binary.BigEndian.Uint64(b))
|
||||
return (usec + pgEpochDeltaUsec) / 1000
|
||||
}
|
||||
|
||||
// pgRawJSONBMap parses a JSONB value into a map[string]string.
|
||||
// Handles both binary wire format (1-byte version header + JSON body)
|
||||
// and text wire format (plain JSON). Returns nil for nil or empty input.
|
||||
func pgRawJSONBMap(b []byte) map[string]string {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Binary format has a version prefix byte (typically 1).
|
||||
// Text format starts directly with '{'.
|
||||
data := b
|
||||
if len(b) > 1 && b[0] != '{' && b[0] != '[' && b[0] != 'n' {
|
||||
data = b[1:] // skip version byte
|
||||
}
|
||||
var m map[string]string
|
||||
_ = json.Unmarshal(data, &m)
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user