pg_catalog_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. package pgserver
  2. import (
  3. "net"
  4. "strconv"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/danfragoso/pizzasql-next/pkg/storage"
  9. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  10. )
  11. func simpleQuery(sql string) *Message {
  12. return &Message{Type: MsgQuery, Data: append([]byte(sql), 0)}
  13. }
  14. // Catalog columns carry PostgreSQL's types over the wire (oid 26, name 19), so
  15. // a driver decodes them without falling back to TEXT.
  16. func TestPgCatalogWireTypes(t *testing.T) {
  17. c, client := newDataConnection(t)
  18. for _, sql := range []string{
  19. "CREATE TABLE wire (id INTEGER PRIMARY KEY, payload JSONB)",
  20. "CREATE INDEX idx_wire_payload ON wire (payload)",
  21. } {
  22. if msgs := runQuery(t, c, client, simpleQuery(sql)); errorCode(msgs[0]) != "" {
  23. t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
  24. }
  25. }
  26. msgs := runQuery(t, c, client, simpleQuery(`
  27. SELECT c.oid, c.relname, c.relkind
  28. FROM pg_catalog.pg_class c
  29. WHERE c.relname = 'wire'`))
  30. if msg := findError(msgs); msg != nil {
  31. t.Fatalf("catalog query failed: %s", errorMessage(msg))
  32. }
  33. rd := findRowDescription(t, msgs)
  34. if got := rowDescriptionOIDs(t, rd.Data); len(got) != 3 || got[0] != 26 || got[1] != 19 || got[2] != 25 {
  35. t.Fatalf("catalog OIDs = %v, want [26 19 25]", got)
  36. }
  37. rows := catalogRows(t, msgs)
  38. if len(rows) != 1 || rows[0][1] != "wire" || rows[0][2] != "r" {
  39. t.Fatalf("pg_class row = %v", rows)
  40. }
  41. if _, err := strconv.ParseUint(rows[0][0], 10, 32); err != nil {
  42. t.Fatalf("oid %q is not numeric: %v", rows[0][0], err)
  43. }
  44. }
  45. // A join across pg_class/pg_namespace/pg_attribute works over the simple-query
  46. // protocol, which is how migration tools list columns.
  47. func TestPgCatalogJoinOverWire(t *testing.T) {
  48. c, client := newDataConnection(t)
  49. for _, sql := range []string{
  50. "CREATE TABLE wire_join (id INTEGER PRIMARY KEY, label TEXT NOT NULL)",
  51. } {
  52. if msgs := runQuery(t, c, client, simpleQuery(sql)); errorCode(msgs[0]) != "" {
  53. t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
  54. }
  55. }
  56. msgs := runQuery(t, c, client, simpleQuery(`
  57. SELECT n.nspname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod)
  58. FROM pg_catalog.pg_class c
  59. JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  60. JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  61. WHERE c.relname = 'wire_join' AND a.attnum > 0
  62. ORDER BY a.attnum`))
  63. if msg := findError(msgs); msg != nil {
  64. t.Fatalf("catalog join failed: %s", errorMessage(msg))
  65. }
  66. rows := catalogRows(t, msgs)
  67. if len(rows) != 2 || rows[0][1] != "id" || rows[0][2] != "integer" || rows[1][1] != "label" {
  68. t.Fatalf("catalog join rows = %v", rows)
  69. }
  70. }
  71. // ORM connections issue SET during startup; the engine acknowledges it and
  72. // SHOW reports the retained search path.
  73. func TestSetAndShowSearchPathOverWire(t *testing.T) {
  74. c, client := newDataConnection(t)
  75. if msgs := runQuery(t, c, client, simpleQuery("SET search_path TO public")); errorCode(msgs[0]) != "" {
  76. t.Fatalf("SET search_path failed: %s", errorMessage(msgs[0]))
  77. }
  78. if msgs := runQuery(t, c, client, simpleQuery("SET TIME ZONE 'UTC'")); errorCode(msgs[0]) != "" {
  79. t.Fatalf("SET TIME ZONE failed: %s", errorMessage(msgs[0]))
  80. }
  81. msgs := runQuery(t, c, client, simpleQuery("SHOW search_path"))
  82. if msg := findError(msgs); msg != nil {
  83. t.Fatalf("SHOW search_path failed: %s", errorMessage(msg))
  84. }
  85. rows := catalogRows(t, msgs)
  86. if len(rows) != 1 || rows[0][0] != "public" {
  87. t.Fatalf("SHOW search_path = %v", rows)
  88. }
  89. msgs = runQuery(t, c, client, simpleQuery("SHOW client_encoding"))
  90. if rows := catalogRows(t, msgs); len(rows) != 1 || !strings.EqualFold(rows[0][0], "UTF8") {
  91. t.Fatalf("SHOW client_encoding = %v", rows)
  92. }
  93. }
  94. // An unknown regclass/regtype reaches the client as SQLSTATE 42P01 over the
  95. // simple-query protocol, both in a projection and inside a WHERE clause, and
  96. // to_regclass returns NULL instead of failing.
  97. func TestPgCatalogUndefinedRelationSQLState(t *testing.T) {
  98. c, client := newDataConnection(t)
  99. for _, sql := range []string{
  100. "CREATE TABLE wire_state (id INTEGER PRIMARY KEY)",
  101. } {
  102. if msgs := runQuery(t, c, client, simpleQuery(sql)); errorCode(msgs[0]) != "" {
  103. t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
  104. }
  105. }
  106. for _, sql := range []string{
  107. `SELECT 'missing_relation'::regclass`,
  108. `SELECT 'missing_type'::regtype`,
  109. `SELECT count(*) FROM pg_catalog.pg_class WHERE oid = 'missing_relation'::regclass`,
  110. `SELECT count(*) FROM public.pg_class`,
  111. } {
  112. msgs := runQuery(t, c, client, simpleQuery(sql))
  113. msg := findError(msgs)
  114. if msg == nil {
  115. t.Fatalf("%s: expected an error, got %v", sql, msgs)
  116. }
  117. if code := errorCode(msg); code != ErrCodeUndefinedTable {
  118. t.Fatalf("%s: SQLSTATE = %q, want %q", sql, code, ErrCodeUndefinedTable)
  119. }
  120. }
  121. msgs := runQuery(t, c, client, simpleQuery(`SELECT to_regclass('missing_relation'), to_regclass('wire_state')`))
  122. if msg := findError(msgs); msg != nil {
  123. t.Fatalf("to_regclass failed: %s", errorMessage(msg))
  124. }
  125. rd := findRowDescription(t, msgs)
  126. if got := rowDescriptionOIDs(t, rd.Data); len(got) != 2 || got[0] != 2205 || got[1] != 2205 {
  127. t.Fatalf("to_regclass OIDs = %v, want regclass 2205 twice", got)
  128. }
  129. rows := catalogRows(t, msgs)
  130. if len(rows) != 1 || rows[0][0] != "<null>" || rows[0][1] != "wire_state" {
  131. t.Fatalf("to_regclass rows = %v, want NULL then wire_state", rows)
  132. }
  133. }
  134. // pg_type's function attributes are regproc (OID 24) with numeric values, not
  135. // the text "-".
  136. func TestPgCatalogRegprocWireType(t *testing.T) {
  137. c, client := newDataConnection(t)
  138. msgs := runQuery(t, c, client, simpleQuery(`SELECT typinput, typoutput FROM pg_type WHERE typname = 'int4'`))
  139. if msg := findError(msgs); msg != nil {
  140. t.Fatalf("pg_type query failed: %s", errorMessage(msg))
  141. }
  142. rd := findRowDescription(t, msgs)
  143. if got := rowDescriptionOIDs(t, rd.Data); len(got) != 2 || got[0] != 24 || got[1] != 24 {
  144. t.Fatalf("regproc OIDs = %v, want [24 24]", got)
  145. }
  146. rows := catalogRows(t, msgs)
  147. if len(rows) != 1 || rows[0][0] != "0" || rows[0][1] != "0" {
  148. t.Fatalf("pg_type regproc values = %v, want 0/0", rows)
  149. }
  150. }
  151. // A catalog-backed predicate in a write must not deadlock the session: the
  152. // engine resolves the snapshot before the locked scan. A deadlock here leaks the
  153. // table gate, so a second connection is probed for reads and writes after the
  154. // error, bounded by a read deadline.
  155. func TestPgCatalogWritePredicateKeepsSecondSessionUsable(t *testing.T) {
  156. kv := testkv.New(t)
  157. pool := kv.Pool(8)
  158. t.Cleanup(func() { pool.Close() })
  159. schema := storage.NewSchemaManager(pool, "pg_catalog_lock")
  160. table := storage.NewTableManager(pool, schema, "pg_catalog_lock")
  161. writer, writerClient := newDataConnectionFor(t, schema, table)
  162. reader, readerClient := newDataConnectionFor(t, schema, table)
  163. run := func(c *Connection, client net.Conn, sql string) []*Message {
  164. t.Helper()
  165. // A blocked statement never answers; bound the wait so a regression
  166. // fails instead of hanging the suite.
  167. if err := client.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
  168. t.Fatalf("set deadline: %v", err)
  169. }
  170. defer func() { _ = client.SetReadDeadline(time.Time{}) }()
  171. return runQuery(t, c, client, simpleQuery(sql))
  172. }
  173. for _, sql := range []string{
  174. "CREATE TABLE locked_write (id INTEGER PRIMARY KEY, v TEXT NOT NULL)",
  175. "INSERT INTO locked_write VALUES (1, 'a')",
  176. } {
  177. if msgs := run(writer, writerClient, sql); errorCode(msgs[0]) != "" {
  178. t.Fatalf("%s: %s", sql, errorMessage(msgs[0]))
  179. }
  180. }
  181. msgs := run(writer, writerClient, `UPDATE locked_write SET v = 'z' WHERE v = 'a' AND 'missing_relation'::regclass IS NOT NULL`)
  182. msg := findError(msgs)
  183. if msg == nil {
  184. t.Fatalf("expected an error for the unknown regclass, got %v", msgs)
  185. }
  186. if code := errorCode(msg); code != ErrCodeUndefinedTable {
  187. t.Fatalf("SQLSTATE = %q, want %q (%s)", code, ErrCodeUndefinedTable, errorMessage(msg))
  188. }
  189. // The failed statement left no partial write and released its locks.
  190. msgs = run(reader, readerClient, `SELECT count(*) FROM locked_write WHERE v = 'z'`)
  191. if msg := findError(msgs); msg != nil {
  192. t.Fatalf("second session SELECT failed: %s", errorMessage(msg))
  193. }
  194. if rows := catalogRows(t, msgs); len(rows) != 1 || rows[0][0] != "0" {
  195. t.Fatalf("updated rows = %v, want 0", rows)
  196. }
  197. msgs = run(reader, readerClient, `INSERT INTO locked_write VALUES (2, 'b')`)
  198. if msg := findError(msgs); msg != nil {
  199. t.Fatalf("second session INSERT failed: %s", errorMessage(msg))
  200. }
  201. }