pgx_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package pgserver
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "reflect"
  7. "strings"
  8. "testing"
  9. "time"
  10. "github.com/jackc/pgx/v5"
  11. "github.com/danfragoso/pizzasql-next/pkg/storage"
  12. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  13. )
  14. // connectPGX starts a wire server over net.Pipe and connects pgx to it, so the
  15. // tests exercise the same Parse/Bind/Describe/Execute sequence a real client
  16. // uses. pgx's default QueryExecMode is cache_statement.
  17. func connectPGX(t *testing.T) *pgx.Conn {
  18. t.Helper()
  19. kv := testkv.New(t)
  20. pool := kv.Pool(4)
  21. t.Cleanup(func() { pool.Close() })
  22. dbManager := storage.NewDatabaseManager(pool, &storage.DatabaseManagerConfig{
  23. DefaultDatabase: "pgx_parameters",
  24. AutoCreate: true,
  25. })
  26. server, client := net.Pipe()
  27. done := make(chan error, 1)
  28. conn := NewConnection(server, dbManager, true)
  29. go func() { done <- conn.Handle() }()
  30. t.Cleanup(func() {
  31. client.Close()
  32. server.Close()
  33. select {
  34. case <-done:
  35. case <-time.After(5 * time.Second):
  36. t.Error("server connection did not close")
  37. }
  38. })
  39. config, err := pgx.ParseConfig("postgres://tester@localhost:5432/pgx_parameters?sslmode=disable")
  40. if err != nil {
  41. t.Fatalf("parse pgx config: %v", err)
  42. }
  43. config.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
  44. return client, nil
  45. }
  46. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  47. t.Cleanup(cancel)
  48. pgxConn, err := pgx.ConnectConfig(ctx, config)
  49. if err != nil {
  50. t.Fatalf("pgx connect: %v", err)
  51. }
  52. t.Cleanup(func() { _ = pgxConn.Close(context.Background()) })
  53. return pgxConn
  54. }
  55. func preparePGXSchema(t *testing.T, conn *pgx.Conn) {
  56. t.Helper()
  57. ctx := context.Background()
  58. for _, sql := range []string{
  59. `CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, doc JSONB)`,
  60. `INSERT INTO t (id, name, doc) VALUES (1, 'alpha', '{"a": 1}')`,
  61. `INSERT INTO t (id, name, doc) VALUES (2, 'beta', '{"b": 2}')`,
  62. } {
  63. if _, err := conn.Exec(ctx, sql); err != nil {
  64. t.Fatalf("%s: %v", sql, err)
  65. }
  66. }
  67. }
  68. // ParameterDescription carries the inferred OID for every parameter, which is
  69. // what pgx validates its argument list against.
  70. func TestPGXDescribesInferredParameterTypes(t *testing.T) {
  71. conn := connectPGX(t)
  72. preparePGXSchema(t, conn)
  73. ctx := context.Background()
  74. cases := []struct {
  75. sql string
  76. want []uint32
  77. }{
  78. {`SELECT $1::INTEGER`, []uint32{23}},
  79. {`SELECT $1::TEXT[]`, []uint32{1009}},
  80. {`SELECT $1::JSONB`, []uint32{3802}},
  81. {`SELECT id FROM t WHERE id = $1`, []uint32{23}},
  82. {`SELECT name FROM t WHERE name = $1`, []uint32{25}},
  83. {`SELECT id FROM t WHERE id = ANY($1)`, []uint32{1007}},
  84. {`SELECT doc FROM t WHERE doc = $1::JSONB`, []uint32{3802}},
  85. {`SELECT doc FROM t WHERE doc = $1`, []uint32{3802}},
  86. {`INSERT INTO t (id, name) VALUES ($1, $2)`, []uint32{23, 25}},
  87. {`UPDATE t SET name = $1 WHERE id = $2`, []uint32{25, 23}},
  88. {`SELECT $1`, []uint32{25}},
  89. }
  90. for i, tc := range cases {
  91. t.Run(tc.sql, func(t *testing.T) {
  92. sd, err := conn.Prepare(ctx, fmt.Sprintf("described_%d", i), tc.sql)
  93. if err != nil {
  94. t.Fatalf("prepare: %v", err)
  95. }
  96. if !reflect.DeepEqual(sd.ParamOIDs, tc.want) {
  97. t.Fatalf("ParamOIDs = %v, want %v", sd.ParamOIDs, tc.want)
  98. }
  99. })
  100. }
  101. }
  102. // The default connection mode (cache_statement) and the other describe-based
  103. // modes bind and execute typed parameters.
  104. func TestPGXDefaultQueryModes(t *testing.T) {
  105. conn := connectPGX(t)
  106. preparePGXSchema(t, conn)
  107. ctx := context.Background()
  108. modes := []struct {
  109. name string
  110. mode pgx.QueryExecMode
  111. }{
  112. {"cache_statement", pgx.QueryExecModeCacheStatement},
  113. {"cache_describe", pgx.QueryExecModeCacheDescribe},
  114. {"describe_exec", pgx.QueryExecModeDescribeExec},
  115. }
  116. for i, tc := range modes {
  117. insertID := int32(10 + i)
  118. t.Run(tc.name, func(t *testing.T) {
  119. var integer int32
  120. if err := conn.QueryRow(ctx, `SELECT $1::INTEGER`, tc.mode, int32(41)).Scan(&integer); err != nil {
  121. t.Fatalf("cast parameter: %v", err)
  122. }
  123. if integer != 41 {
  124. t.Fatalf("cast parameter = %d, want 41", integer)
  125. }
  126. var id int32
  127. if err := conn.QueryRow(ctx, `SELECT id FROM t WHERE id = $1`, tc.mode, int32(1)).Scan(&id); err != nil {
  128. t.Fatalf("comparison parameter: %v", err)
  129. }
  130. if id != 1 {
  131. t.Fatalf("comparison parameter = %d, want 1", id)
  132. }
  133. var ids []int32
  134. rows, err := conn.Query(ctx, `SELECT id FROM t WHERE id = ANY($1) ORDER BY id`, tc.mode, []int32{1, 2})
  135. if err != nil {
  136. t.Fatalf("ANY parameter: %v", err)
  137. }
  138. for rows.Next() {
  139. var value int32
  140. if err := rows.Scan(&value); err != nil {
  141. t.Fatalf("scan ANY row: %v", err)
  142. }
  143. ids = append(ids, value)
  144. }
  145. if err := rows.Err(); err != nil {
  146. t.Fatalf("ANY rows: %v", err)
  147. }
  148. if len(ids) != 2 || ids[0] != 1 || ids[1] != 2 {
  149. t.Fatalf("ANY rows = %v, want [1 2]", ids)
  150. }
  151. tag, err := conn.Exec(ctx, `INSERT INTO t (id, name) VALUES ($1, $2)`, tc.mode, insertID, "inserted")
  152. if err != nil {
  153. t.Fatalf("insert parameter: %v", err)
  154. }
  155. if tag.RowsAffected() != 1 {
  156. t.Fatalf("insert affected %d rows, want 1", tag.RowsAffected())
  157. }
  158. var name string
  159. if err := conn.QueryRow(ctx, `SELECT name FROM t WHERE id = $1`, insertID).Scan(&name); err != nil {
  160. t.Fatalf("read inserted row: %v", err)
  161. }
  162. if name != "inserted" {
  163. t.Fatalf("inserted name = %q", name)
  164. }
  165. var doc string
  166. if err := conn.QueryRow(ctx, `SELECT doc FROM t WHERE doc = $1::JSONB`, tc.mode, map[string]any{"a": 1}).Scan(&doc); err != nil {
  167. t.Fatalf("jsonb parameter: %v", err)
  168. }
  169. if doc != `{"a":1}` {
  170. t.Fatalf("jsonb parameter = %q, want {\"a\":1}", doc)
  171. }
  172. var untyped string
  173. if err := conn.QueryRow(ctx, `SELECT $1`, tc.mode, "hello").Scan(&untyped); err != nil {
  174. t.Fatalf("untyped parameter: %v", err)
  175. }
  176. if untyped != "hello" {
  177. t.Fatalf("untyped parameter = %q", untyped)
  178. }
  179. })
  180. }
  181. }
  182. // A client-supplied Parse OID wins over inference, and Bind decodes the value
  183. // with it.
  184. func TestPGXExplicitParameterOIDWins(t *testing.T) {
  185. conn := connectPGX(t)
  186. ctx := context.Background()
  187. sd, err := conn.PgConn().Prepare(ctx, "explicit_oid", `SELECT $1`, []uint32{20})
  188. if err != nil {
  189. t.Fatalf("prepare: %v", err)
  190. }
  191. if !reflect.DeepEqual(sd.ParamOIDs, []uint32{20}) {
  192. t.Fatalf("ParamOIDs = %v, want [20]", sd.ParamOIDs)
  193. }
  194. result := conn.PgConn().ExecPrepared(ctx, "explicit_oid", [][]byte{[]byte("42")}, []int16{0}, []int16{0}).Read()
  195. if result.Err != nil {
  196. t.Fatalf("execute: %v", result.Err)
  197. }
  198. if len(result.Rows) != 1 || len(result.Rows[0]) != 1 || string(result.Rows[0][0]) != "42" {
  199. t.Fatalf("explicit OID rows = %v, want [[42]]", result.Rows)
  200. }
  201. }
  202. // The described parameter count is the number of placeholders, so a client
  203. // catches a wrong argument count before sending Bind instead of failing on an
  204. // empty declaration.
  205. func TestPGXParameterCountMismatch(t *testing.T) {
  206. conn := connectPGX(t)
  207. preparePGXSchema(t, conn)
  208. ctx := context.Background()
  209. var value int32
  210. for _, tc := range []struct {
  211. name string
  212. args []any
  213. want string
  214. }{
  215. {"missing_argument", nil, "expected 1 arguments, got 0"},
  216. {"extra_argument", []any{int32(1), int32(2)}, "expected 1 arguments, got 2"},
  217. } {
  218. t.Run(tc.name, func(t *testing.T) {
  219. err := conn.QueryRow(ctx, `SELECT id FROM t WHERE id = $1`, tc.args...).Scan(&value)
  220. if err == nil {
  221. t.Fatal("query with a wrong argument count succeeded")
  222. }
  223. if !strings.Contains(err.Error(), tc.want) {
  224. t.Fatalf("error = %v, want %q", err, tc.want)
  225. }
  226. })
  227. }
  228. }