pgx_binary_results_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package pgserver
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "testing"
  7. "github.com/jackc/pgx/v5"
  8. "github.com/jackc/pgx/v5/pgconn"
  9. "github.com/jackc/pgx/v5/pgtype"
  10. )
  11. // preparePGXBinaryResults creates the table the binary-result tests scan.
  12. func preparePGXBinaryResults(t *testing.T, conn *pgx.Conn) {
  13. t.Helper()
  14. ctx := context.Background()
  15. for _, sql := range []string{
  16. `CREATE TABLE b (id INTEGER PRIMARY KEY, small SMALLINT, big BIGINT, flag BOOLEAN, ratio REAL, score DOUBLE,
  17. name TEXT, data BLOB, doc JSONB, tags TEXT[], nums INTEGER[], longs BIGINT[], flags BOOLEAN[], amounts NUMERIC[])`,
  18. `INSERT INTO b VALUES (1, 7, 9000000000, 1, 1.5, 2.25, 'alpha', X'00ff', '{"a": 1}',
  19. '{x,"y z"}', '{1,2}', '{9000000000}', '{t,f}', '{1.10,12345678901234567890}')`,
  20. `INSERT INTO b VALUES (2, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
  21. '{}', '{NULL,3}', '{NULL,-1}', '{f,NULL}', '{NULL,2}')`,
  22. `CREATE TABLE stamps (id INTEGER PRIMARY KEY, created_at TIMESTAMP)`,
  23. `INSERT INTO stamps VALUES (1, '2024-01-02 03:04:05')`,
  24. } {
  25. if _, err := conn.Exec(ctx, sql); err != nil {
  26. t.Fatalf("%s: %v", sql, err)
  27. }
  28. }
  29. }
  30. // pgx asks for per-column binary results with QueryResultFormats. The default
  31. // QueryExecMode still prepares the statement and binds the requested formats,
  32. // so this is the path a pgx application uses when it wants binary results.
  33. func TestPGXBinaryResultScalars(t *testing.T) {
  34. conn := connectPGX(t)
  35. preparePGXBinaryResults(t, conn)
  36. ctx := context.Background()
  37. rows, err := conn.Query(ctx,
  38. `SELECT id, small, big, flag, ratio, score, name FROM b ORDER BY id`,
  39. pgx.QueryResultFormats{1})
  40. if err != nil {
  41. t.Fatalf("query: %v", err)
  42. }
  43. defer rows.Close()
  44. for i, field := range rows.FieldDescriptions() {
  45. if field.Format != 1 {
  46. t.Fatalf("field %d format = %d, want binary", i, field.Format)
  47. }
  48. }
  49. var seen int
  50. for rows.Next() {
  51. var (
  52. id int32
  53. small *int16
  54. big *int64
  55. flag *bool
  56. ratio *float32
  57. score *float64
  58. name *string
  59. )
  60. if err := rows.Scan(&id, &small, &big, &flag, &ratio, &score, &name); err != nil {
  61. t.Fatalf("scan row %d: %v", seen, err)
  62. }
  63. switch seen {
  64. case 0:
  65. if id != 1 || small == nil || *small != 7 {
  66. t.Fatalf("row 1 id/small = %d/%v", id, small)
  67. }
  68. if big == nil || *big != 9000000000 {
  69. t.Fatalf("row 1 big = %v", big)
  70. }
  71. if flag == nil || !*flag || ratio == nil || *ratio != 1.5 || score == nil || *score != 2.25 {
  72. t.Fatalf("row 1 flag/ratio/score = %v/%v/%v", flag, ratio, score)
  73. }
  74. if name == nil || *name != "alpha" {
  75. t.Fatalf("row 1 name = %v", name)
  76. }
  77. case 1:
  78. if id != 2 {
  79. t.Fatalf("row 2 id = %d", id)
  80. }
  81. if small != nil || big != nil || flag != nil || ratio != nil || score != nil || name != nil {
  82. t.Fatalf("row 2 NULL columns = %v %v %v %v %v %v", small, big, flag, ratio, score, name)
  83. }
  84. }
  85. seen++
  86. }
  87. if err := rows.Err(); err != nil {
  88. t.Fatalf("rows: %v", err)
  89. }
  90. if seen != 2 {
  91. t.Fatalf("got %d rows, want 2", seen)
  92. }
  93. }
  94. func TestPGXBinaryResultByteaAndJSONB(t *testing.T) {
  95. conn := connectPGX(t)
  96. preparePGXBinaryResults(t, conn)
  97. ctx := context.Background()
  98. var data []byte
  99. var doc map[string]any
  100. if err := conn.QueryRow(ctx, `SELECT data, doc FROM b WHERE id = 1`, pgx.QueryResultFormats{1}).
  101. Scan(&data, &doc); err != nil {
  102. t.Fatalf("scan bytea/jsonb: %v", err)
  103. }
  104. if len(data) != 2 || data[0] != 0x00 || data[1] != 0xff {
  105. t.Fatalf("bytea = %v, want [0 255]", data)
  106. }
  107. if len(doc) != 1 || doc["a"] != float64(1) {
  108. t.Fatalf("jsonb = %v, want {\"a\": 1}", doc)
  109. }
  110. var raw json.RawMessage
  111. if err := conn.QueryRow(ctx, `SELECT doc FROM b WHERE id = 1`, pgx.QueryResultFormats{1}).
  112. Scan(&raw); err != nil {
  113. t.Fatalf("scan jsonb RawMessage: %v", err)
  114. }
  115. var decoded map[string]any
  116. if err := json.Unmarshal(raw, &decoded); err != nil {
  117. t.Fatalf("jsonb RawMessage %q: %v", raw, err)
  118. }
  119. if decoded["a"] != float64(1) {
  120. t.Fatalf("jsonb RawMessage = %q", raw)
  121. }
  122. }
  123. func TestPGXBinaryResultArrays(t *testing.T) {
  124. conn := connectPGX(t)
  125. preparePGXBinaryResults(t, conn)
  126. ctx := context.Background()
  127. var tags []string
  128. var nums []int32
  129. var longs []int64
  130. var flags []bool
  131. if err := conn.QueryRow(ctx,
  132. `SELECT tags, nums, longs, flags FROM b WHERE id = 1`, pgx.QueryResultFormats{1}).
  133. Scan(&tags, &nums, &longs, &flags); err != nil {
  134. t.Fatalf("scan arrays: %v", err)
  135. }
  136. if len(tags) != 2 || tags[0] != "x" || tags[1] != "y z" {
  137. t.Fatalf("tags = %q", tags)
  138. }
  139. if len(nums) != 2 || nums[0] != 1 || nums[1] != 2 {
  140. t.Fatalf("nums = %v", nums)
  141. }
  142. if len(longs) != 1 || longs[0] != 9000000000 {
  143. t.Fatalf("longs = %v", longs)
  144. }
  145. if len(flags) != 2 || !flags[0] || flags[1] {
  146. t.Fatalf("flags = %v", flags)
  147. }
  148. // An empty array round-trips as an empty, non-nil slice.
  149. var empty []string
  150. if err := conn.QueryRow(ctx, `SELECT tags FROM b WHERE id = 2`, pgx.QueryResultFormats{1}).
  151. Scan(&empty); err != nil {
  152. t.Fatalf("scan empty array: %v", err)
  153. }
  154. if empty == nil || len(empty) != 0 {
  155. t.Fatalf("empty array = %v (nil=%v)", empty, empty == nil)
  156. }
  157. // NULL elements decode into pointer elements, distinct from zero values.
  158. var elements []*int32
  159. if err := conn.QueryRow(ctx, `SELECT nums FROM b WHERE id = 2`, pgx.QueryResultFormats{1}).
  160. Scan(&elements); err != nil {
  161. t.Fatalf("scan NULL array elements: %v", err)
  162. }
  163. if len(elements) != 2 || elements[0] != nil || elements[1] == nil || *elements[1] != 3 {
  164. t.Fatalf("NULL array elements = %v", elements)
  165. }
  166. }
  167. func TestPGXBinaryResultNumericArray(t *testing.T) {
  168. conn := connectPGX(t)
  169. preparePGXBinaryResults(t, conn)
  170. ctx := context.Background()
  171. var amounts []pgtype.Numeric
  172. if err := conn.QueryRow(ctx, `SELECT amounts FROM b WHERE id = 1`, pgx.QueryResultFormats{1}).
  173. Scan(&amounts); err != nil {
  174. t.Fatalf("scan numeric array: %v", err)
  175. }
  176. if len(amounts) != 2 {
  177. t.Fatalf("amounts = %v, want 2 values", amounts)
  178. }
  179. want := []string{"1.10", "12345678901234567890"}
  180. for i, amount := range amounts {
  181. value, err := amount.Value()
  182. if err != nil {
  183. t.Fatalf("numeric %d value: %v", i, err)
  184. }
  185. if value != want[i] {
  186. t.Fatalf("numeric %d = %v, want %q", i, value, want[i])
  187. }
  188. }
  189. // NULL numeric elements stay NULL.
  190. var second []pgtype.Numeric
  191. if err := conn.QueryRow(ctx, `SELECT amounts FROM b WHERE id = 2`, pgx.QueryResultFormats{1}).
  192. Scan(&second); err != nil {
  193. t.Fatalf("scan numeric array with NULL: %v", err)
  194. }
  195. if len(second) != 2 || second[0].Valid || !second[1].Valid {
  196. t.Fatalf("numeric array with NULL = %+v", second)
  197. }
  198. }
  199. // A per-column list mixes text and binary in one result, and pgx decodes each
  200. // column with the format its RowDescription reports.
  201. func TestPGXBinaryResultMixedFormats(t *testing.T) {
  202. conn := connectPGX(t)
  203. preparePGXBinaryResults(t, conn)
  204. ctx := context.Background()
  205. rows, err := conn.Query(ctx, `SELECT name, id, flag FROM b WHERE id = 1`,
  206. pgx.QueryResultFormats{0, 1, 0})
  207. if err != nil {
  208. t.Fatalf("query: %v", err)
  209. }
  210. defer rows.Close()
  211. formats := []int16{}
  212. for _, field := range rows.FieldDescriptions() {
  213. formats = append(formats, field.Format)
  214. }
  215. if !equalInt16(formats, []int16{0, 1, 0}) {
  216. t.Fatalf("formats = %v, want [0 1 0]", formats)
  217. }
  218. var name string
  219. var id int32
  220. var flag string
  221. if !rows.Next() {
  222. t.Fatalf("no row: %v", rows.Err())
  223. }
  224. if err := rows.Scan(&name, &id, &flag); err != nil {
  225. t.Fatalf("scan mixed formats: %v", err)
  226. }
  227. if name != "alpha" || id != 1 || flag != "1" {
  228. t.Fatalf("mixed row = %q %d %q", name, id, flag)
  229. }
  230. if err := rows.Err(); err != nil {
  231. t.Fatalf("rows: %v", err)
  232. }
  233. }
  234. // A binary request for a type without a binary encoder reports SQLSTATE 0A000
  235. // and the connection keeps working for the next query.
  236. func TestPGXBinaryResultUnsupportedOID(t *testing.T) {
  237. conn := connectPGX(t)
  238. preparePGXBinaryResults(t, conn)
  239. ctx := context.Background()
  240. rows, err := conn.Query(ctx, `SELECT created_at FROM stamps`, pgx.QueryResultFormats{1})
  241. if err == nil {
  242. // Execute errors are delivered through the rows, not the Query call.
  243. rows.Close()
  244. err = rows.Err()
  245. }
  246. if err == nil {
  247. t.Fatal("binary timestamptz query succeeded, want 0A000")
  248. }
  249. var pgErr *pgconn.PgError
  250. if !errors.As(err, &pgErr) || pgErr.Code != ErrCodeFeatureNotSupported {
  251. t.Fatalf("error = %v, want SQLSTATE %s", err, ErrCodeFeatureNotSupported)
  252. }
  253. var created string
  254. if err := conn.QueryRow(ctx, `SELECT created_at FROM stamps`).Scan(&created); err != nil {
  255. t.Fatalf("text query after 0A000: %v", err)
  256. }
  257. if created != "2024-01-02 03:04:05" {
  258. t.Fatalf("text timestamp = %q", created)
  259. }
  260. }
  261. // Without an explicit result format pgx keeps getting text results.
  262. func TestPGXTextResultsUnchanged(t *testing.T) {
  263. conn := connectPGX(t)
  264. preparePGXBinaryResults(t, conn)
  265. ctx := context.Background()
  266. rows, err := conn.Query(ctx, `SELECT id, name FROM b WHERE id = 1`)
  267. if err != nil {
  268. t.Fatalf("query: %v", err)
  269. }
  270. defer rows.Close()
  271. for i, field := range rows.FieldDescriptions() {
  272. if field.Format != 0 {
  273. t.Fatalf("field %d format = %d, want text", i, field.Format)
  274. }
  275. }
  276. var id int32
  277. var name string
  278. if !rows.Next() {
  279. t.Fatal("no row")
  280. }
  281. if err := rows.Scan(&id, &name); err != nil {
  282. t.Fatalf("scan: %v", err)
  283. }
  284. if id != 1 || name != "alpha" {
  285. t.Fatalf("text row = %d %q", id, name)
  286. }
  287. }
  288. // RETURNING rows honor the requested binary formats like any other result.
  289. func TestPGXBinaryResultReturning(t *testing.T) {
  290. conn := connectPGX(t)
  291. preparePGXBinaryResults(t, conn)
  292. ctx := context.Background()
  293. var id int32
  294. if err := conn.QueryRow(ctx,
  295. `INSERT INTO stamps (id, created_at) VALUES (2, '2024-02-03 04:05:06') RETURNING id`,
  296. pgx.QueryResultFormats{1}).Scan(&id); err != nil {
  297. t.Fatalf("binary RETURNING: %v", err)
  298. }
  299. if id != 2 {
  300. t.Fatalf("RETURNING id = %d, want 2", id)
  301. }
  302. }
  303. // Binary results coexist with the parameter path from #40: a bound parameter is
  304. // decoded to its text literal and the projected value is returned in binary.
  305. func TestPGXBinaryResultWithParameters(t *testing.T) {
  306. conn := connectPGX(t)
  307. preparePGXBinaryResults(t, conn)
  308. ctx := context.Background()
  309. var id int32
  310. var name string
  311. if err := conn.QueryRow(ctx,
  312. `SELECT id, name FROM b WHERE id = $1`, pgx.QueryResultFormats{1}, int32(1)).
  313. Scan(&id, &name); err != nil {
  314. t.Fatalf("binary query with parameter: %v", err)
  315. }
  316. if id != 1 || name != "alpha" {
  317. t.Fatalf("row = %d %q", id, name)
  318. }
  319. var doubled int64
  320. if err := conn.QueryRow(ctx,
  321. `SELECT $1::BIGINT * 2`, pgx.QueryResultFormats{1}, int64(21)).Scan(&doubled); err != nil {
  322. t.Fatalf("binary result of a parameter expression: %v", err)
  323. }
  324. if doubled != 42 {
  325. t.Fatalf("doubled = %d, want 42", doubled)
  326. }
  327. }