features_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package pgserver
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/binary"
  6. "net"
  7. "testing"
  8. "github.com/danfragoso/pizzasql-next/pkg/executor"
  9. "github.com/danfragoso/pizzasql-next/pkg/storage"
  10. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  11. )
  12. // newDataConnection builds a connection with a real executor backed by testkv so
  13. // simple-query behavior can be asserted end to end.
  14. func newDataConnection(t *testing.T) (*Connection, net.Conn) {
  15. t.Helper()
  16. server, client := net.Pipe()
  17. t.Cleanup(func() {
  18. server.Close()
  19. client.Close()
  20. })
  21. kv := testkv.New(t)
  22. pool := kv.Pool(4)
  23. t.Cleanup(func() { pool.Close() })
  24. schema := storage.NewSchemaManager(pool, "pg_features")
  25. table := storage.NewTableManager(pool, schema, "pg_features")
  26. exec := executor.New(schema, table)
  27. exec.SyncCatalog()
  28. c := &Connection{
  29. conn: server,
  30. reader: bufio.NewReader(server),
  31. writer: bufio.NewWriter(server),
  32. params: map[string]string{"user": "tester"},
  33. statements: make(map[string]*preparedStatement),
  34. portals: make(map[string]*portal),
  35. txStatus: TxStatusIdle,
  36. quiet: true,
  37. executor: exec,
  38. schema: schema,
  39. }
  40. return c, client
  41. }
  42. // dataRowValues decodes a DataRow message into its text values.
  43. func dataRowValues(t *testing.T, msg *Message) []string {
  44. t.Helper()
  45. if msg.Type != MsgDataRow {
  46. t.Fatalf("message type = %c, want DataRow", msg.Type)
  47. }
  48. data := msg.Data
  49. if len(data) < 2 {
  50. t.Fatal("short DataRow")
  51. }
  52. count := int(binary.BigEndian.Uint16(data[:2]))
  53. pos := 2
  54. values := make([]string, 0, count)
  55. for i := 0; i < count; i++ {
  56. if pos+4 > len(data) {
  57. t.Fatal("short DataRow field length")
  58. }
  59. l := int32(binary.BigEndian.Uint32(data[pos : pos+4]))
  60. pos += 4
  61. if l == -1 {
  62. values = append(values, "<null>")
  63. continue
  64. }
  65. values = append(values, string(data[pos:pos+int(l)]))
  66. pos += int(l)
  67. }
  68. return values
  69. }
  70. // commandTag extracts the NUL-terminated tag from a CommandComplete message.
  71. func commandTag(t *testing.T, msg *Message) string {
  72. t.Helper()
  73. if msg.Type != MsgCommandComplete {
  74. t.Fatalf("message type = %c, want CommandComplete", msg.Type)
  75. }
  76. return string(bytes.TrimRight(msg.Data, "\x00"))
  77. }
  78. func TestSimpleQueryInsertReturning(t *testing.T) {
  79. c, client := newDataConnection(t)
  80. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"), 0)})
  81. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t (name) VALUES ('alice') RETURNING id, name"), 0)})
  82. if len(msgs) != 4 {
  83. t.Fatalf("got %d messages, want RowDescription+DataRow+CommandComplete+ReadyForQuery: %v", len(msgs), msgs)
  84. }
  85. if msgs[0].Type != MsgRowDescription {
  86. t.Fatalf("first message = %c, want RowDescription", msgs[0].Type)
  87. }
  88. values := dataRowValues(t, msgs[1])
  89. if len(values) != 2 || values[0] != "1" || values[1] != "alice" {
  90. t.Fatalf("returning row = %v", values)
  91. }
  92. if tag := commandTag(t, msgs[2]); tag != "INSERT 0 1" {
  93. t.Fatalf("command tag = %q, want INSERT 0 1", tag)
  94. }
  95. }
  96. func TestSimpleQueryByteaTextWire(t *testing.T) {
  97. c, client := newDataConnection(t)
  98. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE b (id INTEGER PRIMARY KEY, data BLOB)"), 0)})
  99. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO b (id, data) VALUES (1, X'00FF10')"), 0)})
  100. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT data FROM b WHERE id = 1"), 0)})
  101. values := dataRowValues(t, msgs[1])
  102. if len(values) != 1 || values[0] != `\x00ff10` {
  103. t.Fatalf("bytea wire value = %v, want \\x00ff10", values)
  104. }
  105. }
  106. func TestBlobValueOverridesTextAffinityWireType(t *testing.T) {
  107. result := executor.NewResult("SELECT")
  108. result.AddColumnWithType("settings", "VARCHAR")
  109. result.AddRow([]byte(`{"collect":1}`))
  110. types := wireColumnTypes(result)
  111. if len(types) != 1 || types[0] != "BLOB" {
  112. t.Fatalf("wire types = %v, want [BLOB]", types)
  113. }
  114. if result.ColumnTypes[0] != "VARCHAR" {
  115. t.Fatalf("wire type inference mutated result metadata: %v", result.ColumnTypes)
  116. }
  117. }
  118. func TestCommandTagSelectAndUpdate(t *testing.T) {
  119. c, client := newDataConnection(t)
  120. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)"), 0)})
  121. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("INSERT INTO t VALUES (1, 'a')"), 0)})
  122. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("SELECT * FROM t"), 0)})
  123. if tag := commandTag(t, msgs[len(msgs)-2]); tag != "SELECT 1" {
  124. t.Fatalf("select tag = %q", tag)
  125. }
  126. msgs = runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("UPDATE t SET v='b' RETURNING id"), 0)})
  127. if msgs[0].Type != MsgRowDescription {
  128. t.Fatalf("update returning first message = %c", msgs[0].Type)
  129. }
  130. if tag := commandTag(t, msgs[len(msgs)-2]); tag != "UPDATE 1" {
  131. t.Fatalf("update tag = %q", tag)
  132. }
  133. }
  134. func TestGetCommandTagSelectUsesRowCount(t *testing.T) {
  135. c := &Connection{txStatus: TxStatusIdle}
  136. res := executor.NewResult("SELECT")
  137. res.AddRow(1)
  138. res.AddRow(2)
  139. if tag := c.getCommandTag(parseStmt(t, "SELECT 1"), res); tag != "SELECT 2" {
  140. t.Fatalf("tag = %q, want SELECT 2", tag)
  141. }
  142. }