param_binary.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package pgserver
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "math"
  6. "strconv"
  7. "strings"
  8. "github.com/danfragoso/pizzasql-next/pkg/executor"
  9. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  10. )
  11. // binaryArrayLiteral decodes a PostgreSQL binary array parameter into the typed
  12. // array literal the executor consumes. Element text is re-encoded into array
  13. // input text and canonicalized, so a malformed element fails at Bind with
  14. // 22P02 exactly like a malformed text-format array.
  15. func binaryArrayLiteral(elementType string, value []byte) (string, error) {
  16. text, err := binaryArrayText(elementType, value)
  17. if err != nil {
  18. return "", err
  19. }
  20. arrayType := elementType + "[]"
  21. canonical, err := executor.CanonicalizeArrayText(text, arrayType)
  22. if err != nil {
  23. return "", err
  24. }
  25. return "'" + strings.ReplaceAll(canonical, "'", "''") + "'::" + arrayType, nil
  26. }
  27. // binaryArrayText converts a binary array parameter to PostgreSQL array text.
  28. // The envelope is ndim, hasnull, element OID, one dimension header, then
  29. // length-prefixed elements; every read is bounds-checked and any structural
  30. // problem is reported as invalid text input rather than a partial decode.
  31. func binaryArrayText(elementType string, value []byte) (string, error) {
  32. if len(value) < 12 {
  33. return "", pgerror.InvalidText("binary array parameter is truncated")
  34. }
  35. dimensions := int32(binary.BigEndian.Uint32(value[0:4]))
  36. hasNull := int32(binary.BigEndian.Uint32(value[4:8]))
  37. elementOID := binary.BigEndian.Uint32(value[8:12])
  38. if dimensions < 0 || hasNull < 0 || hasNull > 1 {
  39. return "", pgerror.InvalidText("invalid binary array header")
  40. }
  41. if dimensions > 1 {
  42. // Text-format arrays reject nested braces with 0A000; binary arrays
  43. // report the same unsupported-feature error for parity.
  44. return "", fmt.Errorf("%w: multidimensional arrays are not supported", pgerror.ErrFeatureNotSupported)
  45. }
  46. expectedOID, ok := binaryArrayElementOID(elementType)
  47. if !ok {
  48. return "", fmt.Errorf("%w: binary array element type %s", pgerror.ErrFeatureNotSupported, elementType)
  49. }
  50. if elementOID != expectedOID {
  51. return "", pgerror.InvalidText("binary array element OID %d does not match %s[]", elementOID, elementType)
  52. }
  53. position := 12
  54. count := 0
  55. if dimensions == 1 {
  56. if position+8 > len(value) {
  57. return "", pgerror.InvalidText("binary array parameter is truncated")
  58. }
  59. length := int32(binary.BigEndian.Uint32(value[position : position+4]))
  60. if length < 0 {
  61. return "", pgerror.InvalidText("invalid binary array dimension length")
  62. }
  63. count = int(length)
  64. position += 8 // length and lower bound; elements are read in order
  65. }
  66. // Every element has at least its 4-byte length prefix, so a corrupt count
  67. // cannot drive a long loop over bytes that are not there.
  68. if count > (len(value)-position)/4 {
  69. return "", pgerror.InvalidText("binary array parameter is truncated")
  70. }
  71. var builder strings.Builder
  72. builder.WriteByte('{')
  73. for i := 0; i < count; i++ {
  74. if position+4 > len(value) {
  75. return "", pgerror.InvalidText("binary array parameter is truncated")
  76. }
  77. length := int32(binary.BigEndian.Uint32(value[position : position+4]))
  78. position += 4
  79. if i > 0 {
  80. builder.WriteByte(',')
  81. }
  82. if length == -1 {
  83. if hasNull == 0 {
  84. return "", pgerror.InvalidText("binary array has a NULL element without the null flag")
  85. }
  86. builder.WriteString("NULL")
  87. continue
  88. }
  89. if length < 0 || position+int(length) > len(value) {
  90. return "", pgerror.InvalidText("binary array parameter is truncated")
  91. }
  92. text, err := binaryArrayElementText(elementOID, value[position:position+int(length)])
  93. if err != nil {
  94. return "", err
  95. }
  96. position += int(length)
  97. builder.WriteString(arrayElementLiteral(text))
  98. }
  99. if position != len(value) {
  100. return "", pgerror.InvalidText("binary array parameter has trailing bytes")
  101. }
  102. builder.WriteByte('}')
  103. return builder.String(), nil
  104. }
  105. // binaryArrayElementOID reports the PostgreSQL element OID a supported array
  106. // parameter type must carry in its binary envelope.
  107. func binaryArrayElementOID(elementType string) (uint32, bool) {
  108. switch elementType {
  109. case "TEXT":
  110. return 25, true
  111. case "VARCHAR":
  112. return 1043, true
  113. case "INTEGER":
  114. return 23, true
  115. case "BIGINT":
  116. return 20, true
  117. case "SMALLINT":
  118. return 21, true
  119. case "BOOLEAN":
  120. return 16, true
  121. case "REAL":
  122. return 700, true
  123. case "DOUBLE":
  124. return 701, true
  125. case "NUMERIC":
  126. return 1700, true
  127. default:
  128. return 0, false
  129. }
  130. }
  131. // binaryArrayElementText decodes one element to its PostgreSQL text spelling.
  132. func binaryArrayElementText(elementOID uint32, data []byte) (string, error) {
  133. switch elementOID {
  134. case 25, 1043: // text, varchar
  135. return string(data), nil
  136. case 16: // boolean
  137. if len(data) != 1 || data[0] > 1 {
  138. return "", pgerror.InvalidText("invalid binary boolean array element")
  139. }
  140. if data[0] == 0 {
  141. return "f", nil
  142. }
  143. return "t", nil
  144. case 21: // int2
  145. if len(data) != 2 {
  146. return "", pgerror.InvalidText("invalid binary int2 array element")
  147. }
  148. return strconv.FormatInt(int64(int16(binary.BigEndian.Uint16(data))), 10), nil
  149. case 23: // int4
  150. if len(data) != 4 {
  151. return "", pgerror.InvalidText("invalid binary int4 array element")
  152. }
  153. return strconv.FormatInt(int64(int32(binary.BigEndian.Uint32(data))), 10), nil
  154. case 20: // int8
  155. if len(data) != 8 {
  156. return "", pgerror.InvalidText("invalid binary int8 array element")
  157. }
  158. return strconv.FormatInt(int64(binary.BigEndian.Uint64(data)), 10), nil
  159. case 700: // float4
  160. if len(data) != 4 {
  161. return "", pgerror.InvalidText("invalid binary float4 array element")
  162. }
  163. return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(data))), 'g', -1, 32), nil
  164. case 701: // float8
  165. if len(data) != 8 {
  166. return "", pgerror.InvalidText("invalid binary float8 array element")
  167. }
  168. return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(data)), 'g', -1, 64), nil
  169. case 1700: // numeric
  170. return binaryNumericText(data)
  171. default:
  172. return "", fmt.Errorf("%w: binary array element OID %d", pgerror.ErrFeatureNotSupported, elementOID)
  173. }
  174. }
  175. // arrayElementLiteral quotes one element for array input text. The result is
  176. // still validated by CanonicalizeArrayText, so this only has to be parseable.
  177. func arrayElementLiteral(text string) string {
  178. needsQuote := text == "" || strings.EqualFold(text, "NULL")
  179. for i := 0; i < len(text) && !needsQuote; i++ {
  180. switch text[i] {
  181. case '{', '}', ',', '"', '\\', ' ', '\t', '\n', '\r':
  182. needsQuote = true
  183. }
  184. }
  185. if !needsQuote {
  186. return text
  187. }
  188. var builder strings.Builder
  189. builder.WriteByte('"')
  190. for i := 0; i < len(text); i++ {
  191. if text[i] == '"' || text[i] == '\\' {
  192. builder.WriteByte('\\')
  193. }
  194. builder.WriteByte(text[i])
  195. }
  196. builder.WriteByte('"')
  197. return builder.String()
  198. }
  199. // binaryNumericText converts PostgreSQL's binary numeric representation to its
  200. // exact decimal text. NUMERIC elements stay text in the engine, so the digits
  201. // must not pass through float64: the representation is base-10000 digits, a
  202. // base-10000 weight, a sign, and a display scale.
  203. func binaryNumericText(data []byte) (string, error) {
  204. if len(data) < 8 {
  205. return "", pgerror.InvalidText("binary numeric parameter is truncated")
  206. }
  207. ndigits := int(binary.BigEndian.Uint16(data[0:2]))
  208. weight := int(int16(binary.BigEndian.Uint16(data[2:4])))
  209. sign := binary.BigEndian.Uint16(data[4:6])
  210. scale := int(binary.BigEndian.Uint16(data[6:8]))
  211. if len(data) != 8+ndigits*2 {
  212. return "", pgerror.InvalidText("binary numeric parameter has an inconsistent digit count")
  213. }
  214. if scale > 16383 {
  215. return "", pgerror.InvalidText("binary numeric parameter has an unsupported scale")
  216. }
  217. digits := make([]int, ndigits)
  218. for i := range digits {
  219. digits[i] = int(binary.BigEndian.Uint16(data[8+i*2:]))
  220. if digits[i] > 9999 {
  221. return "", pgerror.InvalidText("binary numeric parameter has an invalid digit")
  222. }
  223. }
  224. switch sign {
  225. case 0x0000, 0x4000:
  226. case 0xC000, 0xD000, 0xF000:
  227. // The engine has no numeric representation for NaN or infinities;
  228. // splicing the bare word into SQL would fail later as an unknown
  229. // column, so reject the value as invalid text input here.
  230. return "", pgerror.InvalidText("binary numeric parameter has an unsupported special value")
  231. default:
  232. return "", pgerror.InvalidText("binary numeric parameter has an invalid sign")
  233. }
  234. var integer strings.Builder
  235. if weight >= 0 {
  236. for position := weight; position >= 0; position-- {
  237. index := weight - position
  238. digit := 0
  239. if index < ndigits {
  240. digit = digits[index]
  241. }
  242. if position == weight {
  243. integer.WriteString(strconv.Itoa(digit))
  244. } else {
  245. fmt.Fprintf(&integer, "%04d", digit)
  246. }
  247. }
  248. }
  249. integerPart := strings.TrimLeft(integer.String(), "0")
  250. if integerPart == "" {
  251. integerPart = "0"
  252. }
  253. var fractional strings.Builder
  254. group := 1
  255. for fractional.Len() < scale {
  256. digit := 0
  257. if index := weight + group; index >= 0 && index < ndigits {
  258. digit = digits[index]
  259. }
  260. fmt.Fprintf(&fractional, "%04d", digit)
  261. group++
  262. }
  263. fractionPart := fractional.String()[:scale]
  264. var result strings.Builder
  265. if sign == 0x4000 {
  266. result.WriteByte('-')
  267. }
  268. result.WriteString(integerPart)
  269. if fractionPart != "" {
  270. result.WriteByte('.')
  271. result.WriteString(fractionPart)
  272. }
  273. return result.String(), nil
  274. }
  275. // binaryFloatText decodes a scalar binary float4 or float8 parameter to the
  276. // decimal text the executor parses.
  277. func binaryFloatText(oid int32, value []byte) (string, error) {
  278. switch oid {
  279. case 700:
  280. if len(value) != 4 {
  281. return "", pgerror.InvalidText("invalid binary float4 parameter")
  282. }
  283. return strconv.FormatFloat(float64(math.Float32frombits(binary.BigEndian.Uint32(value))), 'g', -1, 32), nil
  284. case 701:
  285. if len(value) != 8 {
  286. return "", pgerror.InvalidText("invalid binary float8 parameter")
  287. }
  288. return strconv.FormatFloat(math.Float64frombits(binary.BigEndian.Uint64(value)), 'g', -1, 64), nil
  289. default:
  290. return "", pgerror.InvalidText("unsupported binary floating-point OID %d", oid)
  291. }
  292. }