2
0

pgx_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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. `CREATE TABLE n (id INTEGER PRIMARY KEY, amount NUMERIC)`,
  63. `INSERT INTO n (id, amount) VALUES (1, '1.5'), (2, '2.25')`,
  64. } {
  65. if _, err := conn.Exec(ctx, sql); err != nil {
  66. t.Fatalf("%s: %v", sql, err)
  67. }
  68. }
  69. }
  70. // ParameterDescription carries the inferred OID for every parameter, which is
  71. // what pgx validates its argument list against.
  72. func TestPGXDescribesInferredParameterTypes(t *testing.T) {
  73. conn := connectPGX(t)
  74. preparePGXSchema(t, conn)
  75. ctx := context.Background()
  76. cases := []struct {
  77. sql string
  78. want []uint32
  79. }{
  80. {`SELECT $1::INTEGER`, []uint32{23}},
  81. {`SELECT $1::TEXT[]`, []uint32{1009}},
  82. {`SELECT $1::JSONB`, []uint32{3802}},
  83. {`SELECT id FROM t WHERE id = $1`, []uint32{23}},
  84. {`SELECT name FROM t WHERE name = $1`, []uint32{25}},
  85. {`SELECT id FROM t WHERE id = ANY($1)`, []uint32{1007}},
  86. {`SELECT id FROM t WHERE name = ANY($1)`, []uint32{1009}},
  87. {`SELECT id FROM n WHERE amount = ANY($1)`, []uint32{1231}},
  88. {`SELECT id FROM t LIMIT $1 OFFSET $2`, []uint32{20, 20}},
  89. {`SELECT doc FROM t WHERE doc = $1::JSONB`, []uint32{3802}},
  90. {`SELECT doc FROM t WHERE doc = $1`, []uint32{3802}},
  91. {`INSERT INTO t (id, name) VALUES ($1, $2)`, []uint32{23, 25}},
  92. {`UPDATE t SET name = $1 WHERE id = $2`, []uint32{25, 23}},
  93. {`SELECT $1`, []uint32{25}},
  94. }
  95. for i, tc := range cases {
  96. t.Run(tc.sql, func(t *testing.T) {
  97. sd, err := conn.Prepare(ctx, fmt.Sprintf("described_%d", i), tc.sql)
  98. if err != nil {
  99. t.Fatalf("prepare: %v", err)
  100. }
  101. if !reflect.DeepEqual(sd.ParamOIDs, tc.want) {
  102. t.Fatalf("ParamOIDs = %v, want %v", sd.ParamOIDs, tc.want)
  103. }
  104. })
  105. }
  106. }
  107. // The default connection mode (cache_statement) and the other describe-based
  108. // modes bind and execute typed parameters.
  109. func TestPGXDefaultQueryModes(t *testing.T) {
  110. conn := connectPGX(t)
  111. preparePGXSchema(t, conn)
  112. ctx := context.Background()
  113. modes := []struct {
  114. name string
  115. mode pgx.QueryExecMode
  116. }{
  117. {"cache_statement", pgx.QueryExecModeCacheStatement},
  118. {"cache_describe", pgx.QueryExecModeCacheDescribe},
  119. {"describe_exec", pgx.QueryExecModeDescribeExec},
  120. }
  121. for i, tc := range modes {
  122. insertID := int32(10 + i)
  123. t.Run(tc.name, func(t *testing.T) {
  124. var integer int32
  125. if err := conn.QueryRow(ctx, `SELECT $1::INTEGER`, tc.mode, int32(41)).Scan(&integer); err != nil {
  126. t.Fatalf("cast parameter: %v", err)
  127. }
  128. if integer != 41 {
  129. t.Fatalf("cast parameter = %d, want 41", integer)
  130. }
  131. var id int32
  132. if err := conn.QueryRow(ctx, `SELECT id FROM t WHERE id = $1`, tc.mode, int32(1)).Scan(&id); err != nil {
  133. t.Fatalf("comparison parameter: %v", err)
  134. }
  135. if id != 1 {
  136. t.Fatalf("comparison parameter = %d, want 1", id)
  137. }
  138. var ids []int32
  139. rows, err := conn.Query(ctx, `SELECT id FROM t WHERE id = ANY($1) ORDER BY id`, tc.mode, []int32{1, 2})
  140. if err != nil {
  141. t.Fatalf("ANY parameter: %v", err)
  142. }
  143. for rows.Next() {
  144. var value int32
  145. if err := rows.Scan(&value); err != nil {
  146. t.Fatalf("scan ANY row: %v", err)
  147. }
  148. ids = append(ids, value)
  149. }
  150. if err := rows.Err(); err != nil {
  151. t.Fatalf("ANY rows: %v", err)
  152. }
  153. if len(ids) != 2 || ids[0] != 1 || ids[1] != 2 {
  154. t.Fatalf("ANY rows = %v, want [1 2]", ids)
  155. }
  156. var textIDs []int32
  157. rows, err = conn.Query(ctx, `SELECT id FROM t WHERE name = ANY($1) ORDER BY id`, tc.mode, []string{"alpha", "beta"})
  158. if err != nil {
  159. t.Fatalf("text[] parameter: %v", err)
  160. }
  161. for rows.Next() {
  162. var value int32
  163. if err := rows.Scan(&value); err != nil {
  164. t.Fatalf("scan text[] row: %v", err)
  165. }
  166. textIDs = append(textIDs, value)
  167. }
  168. if err := rows.Err(); err != nil {
  169. t.Fatalf("text[] rows: %v", err)
  170. }
  171. if len(textIDs) != 2 || textIDs[0] != 1 || textIDs[1] != 2 {
  172. t.Fatalf("text[] rows = %v, want [1 2]", textIDs)
  173. }
  174. var numericIDs []int32
  175. rows, err = conn.Query(ctx, `SELECT id FROM n WHERE amount = ANY($1) ORDER BY id`, tc.mode, []float64{1.5})
  176. if err != nil {
  177. t.Fatalf("numeric[] parameter: %v", err)
  178. }
  179. for rows.Next() {
  180. var value int32
  181. if err := rows.Scan(&value); err != nil {
  182. t.Fatalf("scan numeric[] row: %v", err)
  183. }
  184. numericIDs = append(numericIDs, value)
  185. }
  186. if err := rows.Err(); err != nil {
  187. t.Fatalf("numeric[] rows: %v", err)
  188. }
  189. if len(numericIDs) != 1 || numericIDs[0] != 1 {
  190. t.Fatalf("numeric[] rows = %v, want [1]", numericIDs)
  191. }
  192. tag, err := conn.Exec(ctx, `INSERT INTO t (id, name) VALUES ($1, $2)`, tc.mode, insertID, "inserted")
  193. if err != nil {
  194. t.Fatalf("insert parameter: %v", err)
  195. }
  196. if tag.RowsAffected() != 1 {
  197. t.Fatalf("insert affected %d rows, want 1", tag.RowsAffected())
  198. }
  199. var name string
  200. if err := conn.QueryRow(ctx, `SELECT name FROM t WHERE id = $1`, insertID).Scan(&name); err != nil {
  201. t.Fatalf("read inserted row: %v", err)
  202. }
  203. if name != "inserted" {
  204. t.Fatalf("inserted name = %q", name)
  205. }
  206. var doc string
  207. if err := conn.QueryRow(ctx, `SELECT doc FROM t WHERE doc = $1::JSONB`, tc.mode, map[string]any{"a": 1}).Scan(&doc); err != nil {
  208. t.Fatalf("jsonb parameter: %v", err)
  209. }
  210. if doc != `{"a":1}` {
  211. t.Fatalf("jsonb parameter = %q, want {\"a\":1}", doc)
  212. }
  213. var untyped string
  214. if err := conn.QueryRow(ctx, `SELECT $1`, tc.mode, "hello").Scan(&untyped); err != nil {
  215. t.Fatalf("untyped parameter: %v", err)
  216. }
  217. if untyped != "hello" {
  218. t.Fatalf("untyped parameter = %q", untyped)
  219. }
  220. })
  221. }
  222. }
  223. // A client-supplied Parse OID wins over inference, and Bind decodes the value
  224. // with it.
  225. func TestPGXExplicitParameterOIDWins(t *testing.T) {
  226. conn := connectPGX(t)
  227. ctx := context.Background()
  228. sd, err := conn.PgConn().Prepare(ctx, "explicit_oid", `SELECT $1`, []uint32{20})
  229. if err != nil {
  230. t.Fatalf("prepare: %v", err)
  231. }
  232. if !reflect.DeepEqual(sd.ParamOIDs, []uint32{20}) {
  233. t.Fatalf("ParamOIDs = %v, want [20]", sd.ParamOIDs)
  234. }
  235. result := conn.PgConn().ExecPrepared(ctx, "explicit_oid", [][]byte{[]byte("42")}, []int16{0}, []int16{0}).Read()
  236. if result.Err != nil {
  237. t.Fatalf("execute: %v", result.Err)
  238. }
  239. if len(result.Rows) != 1 || len(result.Rows[0]) != 1 || string(result.Rows[0][0]) != "42" {
  240. t.Fatalf("explicit OID rows = %v, want [[42]]", result.Rows)
  241. }
  242. }
  243. // An explicit OID that contradicts the inferred one wins for that parameter
  244. // while the other parameters are still inferred; the base branch would have
  245. // described 0 for the unspecified parameter.
  246. func TestPGXExplicitParameterOIDConflict(t *testing.T) {
  247. conn := connectPGX(t)
  248. preparePGXSchema(t, conn)
  249. ctx := context.Background()
  250. sd, err := conn.PgConn().Prepare(ctx, "conflict_mixed", `SELECT $1::TEXT, id FROM t WHERE id = $2`, []uint32{0, 20})
  251. if err != nil {
  252. t.Fatalf("prepare mixed: %v", err)
  253. }
  254. if want := []uint32{25, 20}; !reflect.DeepEqual(sd.ParamOIDs, want) {
  255. t.Fatalf("ParamOIDs = %v, want %v", sd.ParamOIDs, want)
  256. }
  257. result := conn.PgConn().ExecPrepared(ctx, "conflict_mixed", [][]byte{[]byte("x"), []byte("1")}, []int16{0, 0}, []int16{0}).Read()
  258. if result.Err != nil {
  259. t.Fatalf("execute mixed: %v", result.Err)
  260. }
  261. if len(result.Rows) != 1 || len(result.Rows[0]) != 2 || string(result.Rows[0][0]) != "x" || string(result.Rows[0][1]) != "1" {
  262. t.Fatalf("mixed explicit rows = %v, want [[x 1]]", result.Rows)
  263. }
  264. // The client's OID beats the cast on the same parameter.
  265. sd, err = conn.PgConn().Prepare(ctx, "conflict_cast", `SELECT $1::INTEGER`, []uint32{25})
  266. if err != nil {
  267. t.Fatalf("prepare cast: %v", err)
  268. }
  269. if want := []uint32{25}; !reflect.DeepEqual(sd.ParamOIDs, want) {
  270. t.Fatalf("cast conflict ParamOIDs = %v, want %v", sd.ParamOIDs, want)
  271. }
  272. result = conn.PgConn().ExecPrepared(ctx, "conflict_cast", [][]byte{[]byte("41")}, []int16{0}, []int16{0}).Read()
  273. if result.Err != nil {
  274. t.Fatalf("execute cast: %v", result.Err)
  275. }
  276. if len(result.Rows) != 1 || len(result.Rows[0]) != 1 || string(result.Rows[0][0]) != "41" {
  277. t.Fatalf("cast conflict rows = %v, want [[41]]", result.Rows)
  278. }
  279. }
  280. // The described parameter count is the number of placeholders, so a client
  281. // catches a wrong argument count before sending Bind instead of failing on an
  282. // empty declaration.
  283. func TestPGXParameterCountMismatch(t *testing.T) {
  284. conn := connectPGX(t)
  285. preparePGXSchema(t, conn)
  286. ctx := context.Background()
  287. var value int32
  288. for _, tc := range []struct {
  289. name string
  290. args []any
  291. want string
  292. }{
  293. {"missing_argument", nil, "expected 1 arguments, got 0"},
  294. {"extra_argument", []any{int32(1), int32(2)}, "expected 1 arguments, got 2"},
  295. } {
  296. t.Run(tc.name, func(t *testing.T) {
  297. err := conn.QueryRow(ctx, `SELECT id FROM t WHERE id = $1`, tc.args...).Scan(&value)
  298. if err == nil {
  299. t.Fatal("query with a wrong argument count succeeded")
  300. }
  301. if !strings.Contains(err.Error(), tc.want) {
  302. t.Fatalf("error = %v, want %q", err, tc.want)
  303. }
  304. })
  305. }
  306. }