json_contract_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package pgserver
  2. import (
  3. "bytes"
  4. "strings"
  5. "testing"
  6. )
  7. // jsonErrorField extracts a named field from an ErrorResponse.
  8. func jsonErrorField(t *testing.T, msg *Message, want byte) string {
  9. t.Helper()
  10. if msg == nil || msg.Type != MsgErrorResponse {
  11. t.Fatalf("message type = %v, want ErrorResponse", msg)
  12. }
  13. data := msg.Data
  14. for i := 0; i < len(data); {
  15. if data[i] == 0 {
  16. break
  17. }
  18. field := data[i]
  19. i++
  20. end := bytes.IndexByte(data[i:], 0)
  21. if end < 0 {
  22. break
  23. }
  24. value := string(data[i : i+end])
  25. if field == want {
  26. return value
  27. }
  28. i += end + 1
  29. }
  30. return ""
  31. }
  32. // The JSON (text) contract is TEXT storage plus the JSON1 function surface and
  33. // the -> / ->> operators. The JSONB type is native: it stores canonical JSON
  34. // text and reports OID 3802 (see jsonb_contract_test.go), while JSON keeps the
  35. // text-backed JSON1 behavior exercised here.
  36. func TestJSONTextContractOverSimpleQuery(t *testing.T) {
  37. c, client := newDataConnection(t)
  38. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE settings (id INTEGER PRIMARY KEY, body TEXT)"), 0)})
  39. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`INSERT INTO settings (id, body) VALUES (1, '{"theme":"dark","widgets":[1,2]}')`), 0)})
  40. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT json_extract(body, '$.theme'), json_type(body), json_valid(body) FROM settings WHERE id = 1`), 0)})
  41. values := dataRowValues(t, msgs[1])
  42. if len(values) != 3 || values[0] != "dark" || values[1] != "object" || values[2] != "1" {
  43. t.Fatalf("json round trip = %v, want [dark object 1]", values)
  44. }
  45. // Partial update through json_set, without rewriting the whole document.
  46. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`UPDATE settings SET body = json_set(body, '$.theme', 'light') WHERE id = 1`), 0)})
  47. msgs = runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT json_extract(body, '$.theme'), json_extract(body, '$.widgets[1]') FROM settings WHERE id = 1`), 0)})
  48. values = dataRowValues(t, msgs[1])
  49. if len(values) != 2 || values[0] != "light" || values[1] != "2" {
  50. t.Fatalf("json partial update = %v, want [light 2]", values)
  51. }
  52. // A JSON column keeps the text-backed JSON1 behavior (native JSONB is
  53. // covered separately).
  54. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE docs (id INTEGER PRIMARY KEY, body JSON)"), 0)})
  55. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`INSERT INTO docs (id, body) VALUES (1, '{"a":1}')`), 0)})
  56. msgs = runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT json_extract(body, '$.a') FROM docs`), 0)})
  57. if got := dataRowValues(t, msgs[1]); len(got) != 1 || got[0] != "1" {
  58. t.Fatalf("JSON column extract = %v, want [1]", got)
  59. }
  60. }
  61. func TestJSONInvalidInputOverSimpleQuery(t *testing.T) {
  62. c, client := newDataConnection(t)
  63. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT json_extract('not json', '$.a')`), 0)})
  64. if len(msgs) != 2 || msgs[0].Type != MsgErrorResponse {
  65. t.Fatalf("expected ErrorResponse+ReadyForQuery, got %v", msgs)
  66. }
  67. if msg := jsonErrorField(t, msgs[0], ErrorFieldMessage); !strings.Contains(msg, "malformed JSON") {
  68. t.Fatalf("error message = %q, want a malformed-JSON error", msg)
  69. }
  70. // json_valid is the non-throwing way to probe a value.
  71. msgs = runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT json_valid('not json')`), 0)})
  72. if got := dataRowValues(t, msgs[1]); len(got) != 1 || got[0] != "0" {
  73. t.Fatalf("json_valid = %v, want [0]", got)
  74. }
  75. }
  76. func TestJSONArrowOperatorsOverSimpleQuery(t *testing.T) {
  77. c, client := newDataConnection(t)
  78. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE settings (id INTEGER PRIMARY KEY, body TEXT)"), 0)})
  79. runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`INSERT INTO settings (id, body) VALUES (1, '{"theme":"dark","widgets":[1,2]}')`), 0)})
  80. msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT body ->> 'theme', body -> 'widgets' -> 0 FROM settings WHERE id = 1`), 0)})
  81. values := dataRowValues(t, msgs[1])
  82. if len(values) != 2 || values[0] != "dark" || values[1] != "1" {
  83. t.Fatalf("arrow operators = %v, want [dark 1]", values)
  84. }
  85. // Malformed JSON through an arrow operator is the same error json_extract throws.
  86. msgs = runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(`SELECT 'not json' ->> 'a'`), 0)})
  87. if len(msgs) != 2 || msgs[0].Type != MsgErrorResponse {
  88. t.Fatalf("expected ErrorResponse+ReadyForQuery, got %v", msgs)
  89. }
  90. if msg := jsonErrorField(t, msgs[0], ErrorFieldMessage); !strings.Contains(msg, "malformed JSON") {
  91. t.Fatalf("error message = %q, want a malformed-JSON error", msg)
  92. }
  93. }