features_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. package pgserver
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/binary"
  6. "net"
  7. "strings"
  8. "testing"
  9. "github.com/danfragoso/pizzasql-next/pkg/executor"
  10. "github.com/danfragoso/pizzasql-next/pkg/storage"
  11. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  12. )
  13. // newDataConnection builds a connection with a real executor backed by testkv so
  14. // simple-query behavior can be asserted end to end.
  15. func newDataConnection(t *testing.T) (*Connection, net.Conn) {
  16. t.Helper()
  17. server, client := net.Pipe()
  18. t.Cleanup(func() {
  19. server.Close()
  20. client.Close()
  21. })
  22. kv := testkv.New(t)
  23. pool := kv.Pool(4)
  24. t.Cleanup(func() { pool.Close() })
  25. schema := storage.NewSchemaManager(pool, "pg_features")
  26. table := storage.NewTableManager(pool, schema, "pg_features")
  27. exec := executor.New(schema, table)
  28. exec.SyncCatalog()
  29. c := &Connection{
  30. conn: server,
  31. reader: bufio.NewReader(server),
  32. writer: bufio.NewWriter(server),
  33. params: map[string]string{"user": "tester"},
  34. statements: make(map[string]*preparedStatement),
  35. portals: make(map[string]*portal),
  36. txStatus: TxStatusIdle,
  37. quiet: true,
  38. executor: exec,
  39. schema: schema,
  40. }
  41. return c, client
  42. }
  43. // dataRowValues decodes a DataRow message into its text values.
  44. func dataRowValues(t *testing.T, msg *Message) []string {
  45. t.Helper()
  46. if msg.Type != MsgDataRow {
  47. t.Fatalf("message type = %c, want DataRow", msg.Type)
  48. }
  49. data := msg.Data
  50. if len(data) < 2 {
  51. t.Fatal("short DataRow")
  52. }
  53. count := int(binary.BigEndian.Uint16(data[:2]))
  54. pos := 2
  55. values := make([]string, 0, count)
  56. for i := 0; i < count; i++ {
  57. if pos+4 > len(data) {
  58. t.Fatal("short DataRow field length")
  59. }
  60. l := int32(binary.BigEndian.Uint32(data[pos : pos+4]))
  61. pos += 4
  62. if l == -1 {
  63. values = append(values, "<null>")
  64. continue
  65. }
  66. values = append(values, string(data[pos:pos+int(l)]))
  67. pos += int(l)
  68. }
  69. return values
  70. }
  71. // A durable implicit commit may report its LSN between result completion and
  72. // ReadyForQuery. Keep result-shape assertions independent of status updates.
  73. func resultMessages(messages []*Message) []*Message {
  74. var result []*Message
  75. for _, message := range messages {
  76. if message.Type != MsgParameterStatus {
  77. result = append(result, message)
  78. }
  79. }
  80. return result
  81. }
  82. // commandTag extracts the NUL-terminated tag from a CommandComplete message.
  83. func commandTag(t *testing.T, msg *Message) string {
  84. t.Helper()
  85. if msg.Type != MsgCommandComplete {
  86. t.Fatalf("message type = %c, want CommandComplete", msg.Type)
  87. }
  88. return string(bytes.TrimRight(msg.Data, "\x00"))
  89. }
  90. func TestSimpleQueryInsertReturning(t *testing.T) {
  91. c, client := newDataConnection(t)
  92. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"), 0)})
  93. msgs := resultMessages(runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t (name) VALUES ('alice') RETURNING id, name"), 0)}))
  94. if len(msgs) != 4 {
  95. t.Fatalf("got %d messages, want RowDescription+DataRow+CommandComplete+ReadyForQuery: %v", len(msgs), msgs)
  96. }
  97. if msgs[0].Type != MsgRowDescription {
  98. t.Fatalf("first message = %c, want RowDescription", msgs[0].Type)
  99. }
  100. values := dataRowValues(t, msgs[1])
  101. if len(values) != 2 || values[0] != "1" || values[1] != "alice" {
  102. t.Fatalf("returning row = %v", values)
  103. }
  104. if tag := commandTag(t, msgs[2]); tag != "INSERT 0 1" {
  105. t.Fatalf("command tag = %q, want INSERT 0 1", tag)
  106. }
  107. }
  108. func TestSimpleQueryByteaTextWire(t *testing.T) {
  109. c, client := newDataConnection(t)
  110. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE b (id INTEGER PRIMARY KEY, data BLOB)"), 0)})
  111. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO b (id, data) VALUES (1, X'00FF10')"), 0)})
  112. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT data FROM b WHERE id = 1"), 0)})
  113. values := dataRowValues(t, msgs[1])
  114. if len(values) != 1 || values[0] != `\x00ff10` {
  115. t.Fatalf("bytea wire value = %v, want \\x00ff10", values)
  116. }
  117. }
  118. func TestBlobValueOverridesTextAffinityWireType(t *testing.T) {
  119. result := executor.NewResult("SELECT")
  120. result.AddColumnWithType("settings", "VARCHAR")
  121. result.AddRow([]byte(`{"collect":1}`))
  122. types := wireColumnTypes(result)
  123. if len(types) != 1 || types[0] != "BLOB" {
  124. t.Fatalf("wire types = %v, want [BLOB]", types)
  125. }
  126. if result.ColumnTypes[0] != "VARCHAR" {
  127. t.Fatalf("wire type inference mutated result metadata: %v", result.ColumnTypes)
  128. }
  129. }
  130. func TestCommandTagSelectAndUpdate(t *testing.T) {
  131. c, client := newDataConnection(t)
  132. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)"), 0)})
  133. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t VALUES (1, 'a')"), 0)})
  134. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT * FROM t"), 0)})
  135. if tag := commandTag(t, msgs[len(msgs)-2]); tag != "SELECT 1" {
  136. t.Fatalf("select tag = %q", tag)
  137. }
  138. msgs = resultMessages(runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("UPDATE t SET v='b' RETURNING id"), 0)}))
  139. if msgs[0].Type != MsgRowDescription {
  140. t.Fatalf("update returning first message = %c", msgs[0].Type)
  141. }
  142. if tag := commandTag(t, msgs[len(msgs)-2]); tag != "UPDATE 1" {
  143. t.Fatalf("update tag = %q", tag)
  144. }
  145. }
  146. func TestGetCommandTagSelectUsesRowCount(t *testing.T) {
  147. c := &Connection{txStatus: TxStatusIdle}
  148. res := executor.NewResult("SELECT")
  149. res.AddRow(1)
  150. res.AddRow(2)
  151. if tag := c.getCommandTag(parseStmt(t, "SELECT 1"), res); tag != "SELECT 2" {
  152. t.Fatalf("tag = %q, want SELECT 2", tag)
  153. }
  154. }
  155. // errorMessage extracts the human-readable message from an ErrorResponse.
  156. func errorMessage(msg *Message) string {
  157. if msg == nil || msg.Type != MsgErrorResponse {
  158. return ""
  159. }
  160. data := msg.Data
  161. for i := 0; i < len(data); {
  162. if data[i] == 0 {
  163. break
  164. }
  165. field := data[i]
  166. i++
  167. end := bytes.IndexByte(data[i:], 0)
  168. if end < 0 {
  169. break
  170. }
  171. value := string(data[i : i+end])
  172. if field == ErrorFieldMessage {
  173. return value
  174. }
  175. i += end + 1
  176. }
  177. return ""
  178. }
  179. func TestSimpleQueryUniqueViolationErrorCode(t *testing.T) {
  180. c, client := newDataConnection(t)
  181. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)"), 0)})
  182. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t VALUES (1, 'a@x')"), 0)})
  183. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t VALUES (2, 'a@x')"), 0)})
  184. if len(msgs) != 2 {
  185. t.Fatalf("got %d messages, want ErrorResponse+ReadyForQuery", len(msgs))
  186. }
  187. if code := errorCode(msgs[0]); code != ErrCodeUniqueViolation {
  188. t.Fatalf("error code = %q, want %q", code, ErrCodeUniqueViolation)
  189. }
  190. if message := errorMessage(msgs[0]); !strings.Contains(message, "uniq_t_email") {
  191. t.Fatalf("error message = %q, want constraint uniq_t_email", message)
  192. }
  193. }
  194. func TestExtendedQueryUniqueViolationErrorCode(t *testing.T) {
  195. c, client := newDataConnection(t)
  196. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY, email TEXT UNIQUE)"), 0)})
  197. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t VALUES (1, 'a@x')"), 0)})
  198. parse := NewMessageBuilder()
  199. parse.WriteString("")
  200. parse.WriteString("INSERT INTO t VALUES (2, 'a@x')")
  201. parse.WriteInt16(0)
  202. bind := NewMessageBuilder()
  203. bind.WriteString("")
  204. bind.WriteString("")
  205. bind.WriteInt16(0) // parameter format codes
  206. bind.WriteInt16(0) // parameter values
  207. bind.WriteInt16(0) // result format codes
  208. execute := NewMessageBuilder()
  209. execute.WriteString("")
  210. execute.WriteInt32(0)
  211. done := make(chan error, 1)
  212. go func() {
  213. if err := c.handleParse(&Message{Type: MsgParse, Data: parse.Bytes()}); err != nil {
  214. done <- err
  215. return
  216. }
  217. if err := c.handleBind(&Message{Type: MsgBind, Data: bind.Bytes()}); err != nil {
  218. done <- err
  219. return
  220. }
  221. done <- c.handleExecute(&Message{Type: MsgExecute, Data: execute.Bytes()})
  222. }()
  223. reader := bufio.NewReader(client)
  224. var errMsg *Message
  225. for errMsg == nil {
  226. msg, err := ReadMessage(reader)
  227. if err != nil {
  228. t.Fatalf("read response: %v", err)
  229. }
  230. if msg.Type == MsgErrorResponse {
  231. errMsg = msg
  232. }
  233. }
  234. if err := <-done; err != nil {
  235. t.Fatalf("extended query: %v", err)
  236. }
  237. if code := errorCode(errMsg); code != ErrCodeUniqueViolation {
  238. t.Fatalf("error code = %q, want %q", code, ErrCodeUniqueViolation)
  239. }
  240. if message := errorMessage(errMsg); !strings.Contains(message, "uniq_t_email") {
  241. t.Fatalf("error message = %q, want constraint uniq_t_email", message)
  242. }
  243. }
  244. func TestCompositePrimaryKeyDDLErrorCode(t *testing.T) {
  245. c, client := newDataConnection(t)
  246. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (a TEXT, b TEXT, PRIMARY KEY (a,b))"), 0)})
  247. if len(msgs) != 2 {
  248. t.Fatalf("got %d messages, want ErrorResponse+ReadyForQuery", len(msgs))
  249. }
  250. if code := errorCode(msgs[0]); code != ErrCodeFeatureNotSupported {
  251. t.Fatalf("error code = %q, want %q", code, ErrCodeFeatureNotSupported)
  252. }
  253. if message := errorMessage(msgs[0]); !strings.Contains(message, "composite primary key") {
  254. t.Fatalf("error message = %q, want a composite primary key error", message)
  255. }
  256. }