| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- package pgserver
- import (
- "encoding/binary"
- "fmt"
- "math"
- "strconv"
- "strings"
- "github.com/danfragoso/pizzasql-next/pkg/executor"
- "github.com/danfragoso/pizzasql-next/pkg/pgerror"
- )
- // binaryArrayLiteral decodes a PostgreSQL binary array parameter into the typed
- // array literal the executor consumes. Element text is re-encoded into array
- // input text and canonicalized, so a malformed element fails at Bind with
- // 22P02 exactly like a malformed text-format array.
- func binaryArrayLiteral(elementType string, value []byte) (string, error) {
- text, err := binaryArrayText(elementType, value)
- if err != nil {
- return "", err
- }
- arrayType := elementType + "[]"
- canonical, err := executor.CanonicalizeArrayText(text, arrayType)
- if err != nil {
- return "", err
- }
- return "'" + strings.ReplaceAll(canonical, "'", "''") + "'::" + arrayType, nil
- }
- // binaryArrayText converts a binary array parameter to PostgreSQL array text.
- // The envelope is ndim, hasnull, element OID, one dimension header, then
- // length-prefixed elements; every read is bounds-checked and any structural
- // problem is reported as invalid text input rather than a partial decode.
- func binaryArrayText(elementType string, value []byte) (string, error) {
- if len(value) < 12 {
- return "", pgerror.InvalidText("binary array parameter is truncated")
- }
- dimensions := int32(binary.BigEndian.Uint32(value[0:4]))
- hasNull := int32(binary.BigEndian.Uint32(value[4:8]))
- elementOID := binary.BigEndian.Uint32(value[8:12])
- if dimensions < 0 || hasNull < 0 || hasNull > 1 {
- return "", pgerror.InvalidText("invalid binary array header")
- }
- if dimensions > 1 {
- // Text-format arrays reject nested braces with 0A000; binary arrays
- // report the same unsupported-feature error for parity.
- return "", fmt.Errorf("%w: multidimensional arrays are not supported", pgerror.ErrFeatureNotSupported)
- }
- expectedOID, ok := binaryArrayElementOID(elementType)
- if !ok {
- return "", fmt.Errorf("%w: binary array element type %s", pgerror.ErrFeatureNotSupported, elementType)
- }
- if elementOID != expectedOID {
- return "", pgerror.InvalidText("binary array element OID %d does not match %s[]", elementOID, elementType)
- }
- position := 12
- count := 0
- if dimensions == 1 {
- if position+8 > len(value) {
- return "", pgerror.InvalidText("binary array parameter is truncated")
- }
- length := int32(binary.BigEndian.Uint32(value[position : position+4]))
- if length < 0 {
- return "", pgerror.InvalidText("invalid binary array dimension length")
- }
- count = int(length)
- position += 8 // length and lower bound; elements are read in order
- }
- // Every element has at least its 4-byte length prefix, so a corrupt count
- // cannot drive a long loop over bytes that are not there.
- if count > (len(value)-position)/4 {
- return "", pgerror.InvalidText("binary array parameter is truncated")
- }
- var builder strings.Builder
- builder.WriteByte('{')
- for i := 0; i < count; i++ {
- if position+4 > len(value) {
- return "", pgerror.InvalidText("binary array parameter is truncated")
- }
- length := int32(binary.BigEndian.Uint32(value[position : position+4]))
- position += 4
- if i > 0 {
- builder.WriteByte(',')
- }
- if length == -1 {
- if hasNull == 0 {
- return "", pgerror.InvalidText("binary array has a NULL element without the null flag")
- }
- builder.WriteString("NULL")
- continue
- }
- if length < 0 || position+int(length) > len(value) {
- return "", pgerror.InvalidText("binary array parameter is truncated")
- }
- text, err := binaryArrayElementText(elementOID, value[position:position+int(length)])
- if err != nil {
- return "", err
- }
- position += int(length)
- builder.WriteString(arrayElementLiteral(text))
- }
- if position != len(value) {
- return "", pgerror.InvalidText("binary array parameter has trailing bytes")
- }
- builder.WriteByte('}')
- return builder.String(), nil
- }
- // binaryArrayElementOID reports the PostgreSQL element OID a supported array
- // parameter type must carry in its binary envelope.
- func binaryArrayElementOID(elementType string) (uint32, bool) {
- switch elementType {
- case "TEXT":
- return 25, true
- case "VARCHAR":
- return 1043, true
- case "INTEGER":
- return 23, true
- case "BIGINT":
- return 20, true
- case "SMALLINT":
- return 21, true
- case "BOOLEAN":
- return 16, true
- case "REAL":
- return 700, true
- case "DOUBLE":
- return 701, true
- case "NUMERIC":
- return 1700, true
- default:
- return 0, false
- }
- }
- // binaryArrayElementText decodes one element to its PostgreSQL text spelling.
- func binaryArrayElementText(elementOID uint32, data []byte) (string, error) {
- switch elementOID {
- case 25, 1043: // text, varchar
- return string(data), nil
- case 16: // boolean
- if len(data) != 1 || data[0] > 1 {
- return "", pgerror.InvalidText("invalid binary boolean array element")
- }
- if data[0] == 0 {
- return "f", nil
- }
- return "t", nil
- case 21: // int2
- if len(data) != 2 {
- return "", pgerror.InvalidText("invalid binary int2 array element")
- }
- return strconv.FormatInt(int64(int16(binary.BigEndian.Uint16(data))), 10), nil
- case 23: // int4
- if len(data) != 4 {
- return "", pgerror.InvalidText("invalid binary int4 array element")
- }
- return strconv.FormatInt(int64(int32(binary.BigEndian.Uint32(data))), 10), nil
- case 20: // int8
- if len(data) != 8 {
- return "", pgerror.InvalidText("invalid binary int8 array element")
- }
- return strconv.FormatInt(int64(binary.BigEndian.Uint64(data)), 10), nil
- case 700: // float4
- if len(data) != 4 {
- return "", pgerror.InvalidText("invalid binary float4 array element")
- }
- return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(data))), 'g', -1, 32), nil
- case 701: // float8
- if len(data) != 8 {
- return "", pgerror.InvalidText("invalid binary float8 array element")
- }
- return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(data)), 'g', -1, 64), nil
- case 1700: // numeric
- return binaryNumericText(data)
- default:
- return "", fmt.Errorf("%w: binary array element OID %d", pgerror.ErrFeatureNotSupported, elementOID)
- }
- }
- // arrayElementLiteral quotes one element for array input text. The result is
- // still validated by CanonicalizeArrayText, so this only has to be parseable.
- func arrayElementLiteral(text string) string {
- needsQuote := text == "" || strings.EqualFold(text, "NULL")
- for i := 0; i < len(text) && !needsQuote; i++ {
- switch text[i] {
- case '{', '}', ',', '"', '\\', ' ', '\t', '\n', '\r':
- needsQuote = true
- }
- }
- if !needsQuote {
- return text
- }
- var builder strings.Builder
- builder.WriteByte('"')
- for i := 0; i < len(text); i++ {
- if text[i] == '"' || text[i] == '\\' {
- builder.WriteByte('\\')
- }
- builder.WriteByte(text[i])
- }
- builder.WriteByte('"')
- return builder.String()
- }
- // binaryNumericText converts PostgreSQL's binary numeric representation to its
- // exact decimal text. NUMERIC elements stay text in the engine, so the digits
- // must not pass through float64: the representation is base-10000 digits, a
- // base-10000 weight, a sign, and a display scale.
- func binaryNumericText(data []byte) (string, error) {
- if len(data) < 8 {
- return "", pgerror.InvalidText("binary numeric parameter is truncated")
- }
- ndigits := int(binary.BigEndian.Uint16(data[0:2]))
- weight := int(int16(binary.BigEndian.Uint16(data[2:4])))
- sign := binary.BigEndian.Uint16(data[4:6])
- scale := int(binary.BigEndian.Uint16(data[6:8]))
- if len(data) != 8+ndigits*2 {
- return "", pgerror.InvalidText("binary numeric parameter has an inconsistent digit count")
- }
- if scale > 16383 {
- return "", pgerror.InvalidText("binary numeric parameter has an unsupported scale")
- }
- digits := make([]int, ndigits)
- for i := range digits {
- digits[i] = int(binary.BigEndian.Uint16(data[8+i*2:]))
- if digits[i] > 9999 {
- return "", pgerror.InvalidText("binary numeric parameter has an invalid digit")
- }
- }
- switch sign {
- case 0x0000, 0x4000:
- case 0xC000, 0xD000, 0xF000:
- // The engine has no numeric representation for NaN or infinities;
- // splicing the bare word into SQL would fail later as an unknown
- // column, so reject the value as invalid text input here.
- return "", pgerror.InvalidText("binary numeric parameter has an unsupported special value")
- default:
- return "", pgerror.InvalidText("binary numeric parameter has an invalid sign")
- }
- var integer strings.Builder
- if weight >= 0 {
- for position := weight; position >= 0; position-- {
- index := weight - position
- digit := 0
- if index < ndigits {
- digit = digits[index]
- }
- if position == weight {
- integer.WriteString(strconv.Itoa(digit))
- } else {
- fmt.Fprintf(&integer, "%04d", digit)
- }
- }
- }
- integerPart := strings.TrimLeft(integer.String(), "0")
- if integerPart == "" {
- integerPart = "0"
- }
- var fractional strings.Builder
- group := 1
- for fractional.Len() < scale {
- digit := 0
- if index := weight + group; index >= 0 && index < ndigits {
- digit = digits[index]
- }
- fmt.Fprintf(&fractional, "%04d", digit)
- group++
- }
- fractionPart := fractional.String()[:scale]
- var result strings.Builder
- if sign == 0x4000 {
- result.WriteByte('-')
- }
- result.WriteString(integerPart)
- if fractionPart != "" {
- result.WriteByte('.')
- result.WriteString(fractionPart)
- }
- return result.String(), nil
- }
- // binaryFloatText decodes a scalar binary float4 or float8 parameter to the
- // decimal text the executor parses.
- func binaryFloatText(oid int32, value []byte) (string, error) {
- switch oid {
- case 700:
- if len(value) != 4 {
- return "", pgerror.InvalidText("invalid binary float4 parameter")
- }
- return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(value))), 'g', -1, 32), nil
- case 701:
- if len(value) != 8 {
- return "", pgerror.InvalidText("invalid binary float8 parameter")
- }
- return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(value)), 'g', -1, 64), nil
- default:
- return "", pgerror.InvalidText("unsupported binary floating-point OID %d", oid)
- }
- }
|