json_operator_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package executor
  2. import (
  3. "strings"
  4. "testing"
  5. )
  6. func TestJSONArrowObjectAndArrayAccess(t *testing.T) {
  7. _, schema, table := newTestDB(t)
  8. e := newExec(schema, table)
  9. tests := []struct {
  10. name string
  11. sql string
  12. want interface{}
  13. }{
  14. {"object member as JSON text", `SELECT '{"a":1,"s":"x"}' -> 'a'`, "1"},
  15. {"string member keeps JSON quotes", `SELECT '{"a":1,"s":"x"}' -> 's'`, `"x"`},
  16. {"string member as SQL text", `SELECT '{"a":1,"s":"x"}' ->> 's'`, "x"},
  17. {"object member as JSON text", `SELECT '{"b":[10,20]}' -> 'b'`, "[10,20]"},
  18. {"array element as JSON text", `SELECT '[10,20,30]' -> 1`, "20"},
  19. {"array element as SQL number", `SELECT '[10,20,30]' ->> 1`, int64(20)},
  20. {"array element as JSON text", `SELECT '["a","b"]' -> 0`, `"a"`},
  21. {"negative array index", `SELECT '[10,20,30]' ->> -1`, int64(30)},
  22. {"nested chaining", `SELECT '{"a":{"b":[{"c":7}]}}' -> 'a' -> 'b' -> 0 ->> 'c'`, int64(7)},
  23. {"numeric key selects an array index", `SELECT '[10,20]' -> 1`, "20"},
  24. {"string key names an object member", `SELECT '{"1":"one"}' -> '1'`, `"one"`},
  25. {"numeric key on an object is missing", `SELECT '{"1":"one"}' -> 1`, nil},
  26. {"json null as JSON text", `SELECT '{"a":null}' -> 'a'`, "null"},
  27. {"json null as SQL NULL", `SELECT '{"a":null}' ->> 'a'`, nil},
  28. {"json true as JSON text", `SELECT '{"a":true}' -> 'a'`, "true"},
  29. {"json true as SQL integer", `SELECT '{"a":true}' ->> 'a'`, int64(1)},
  30. {"json false as SQL integer", `SELECT '{"a":false}' ->> 'a'`, int64(0)},
  31. {"path-style key", `SELECT '{"a":{"b":2}}' -> '$.a' ->> 'b'`, int64(2)},
  32. {"whole document path", `SELECT '{"a":1}' ->> '$'`, `{"a":1}`},
  33. {"missing object key", `SELECT '{"a":1}' -> 'b'`, nil},
  34. {"missing object key as SQL", `SELECT '{"a":1}' ->> 'b'`, nil},
  35. {"array index past the end", `SELECT '[1,2]' -> 5`, nil},
  36. {"NULL document", `SELECT NULL -> 'a'`, nil},
  37. {"NULL document as SQL", `SELECT NULL ->> 'a'`, nil},
  38. {"NULL key", `SELECT '{"a":1}' -> NULL`, nil},
  39. }
  40. for _, tt := range tests {
  41. t.Run(tt.name, func(t *testing.T) {
  42. res := execMust(t, e, tt.sql)
  43. got := res.Rows[0][0]
  44. if got != tt.want {
  45. t.Fatalf("%s = %v (%T), want %v (%T)", tt.sql, got, got, tt.want, tt.want)
  46. }
  47. })
  48. }
  49. }
  50. func TestJSONArrowMalformedInputIsAnError(t *testing.T) {
  51. _, schema, table := newTestDB(t)
  52. e := newExec(schema, table)
  53. for _, sql := range []string{
  54. `SELECT 'not json' -> 'a'`,
  55. `SELECT 'not json' ->> 'a'`,
  56. `SELECT '{"a":1}' -> '$.['`,
  57. } {
  58. if _, err := execSQL(e, sql); err == nil {
  59. t.Fatalf("%s: expected an error", sql)
  60. } else if !strings.Contains(err.Error(), "JSON") {
  61. t.Fatalf("%s: unexpected error: %v", sql, err)
  62. }
  63. }
  64. }
  65. func TestJSONArrowInWhereOrderByAndUpdate(t *testing.T) {
  66. _, schema, table := newTestDB(t)
  67. e := newExec(schema, table)
  68. execMust(t, e, "CREATE TABLE docs (id INTEGER PRIMARY KEY, doc TEXT, v TEXT)")
  69. execMust(t, e, `INSERT INTO docs VALUES (1, '{"kind":"a","n":1}', NULL)`)
  70. execMust(t, e, `INSERT INTO docs VALUES (2, '{"kind":"b","n":2}', NULL)`)
  71. execMust(t, e, `INSERT INTO docs VALUES (3, '{"kind":"c","n":3}', NULL)`)
  72. res := execMust(t, e, `SELECT id FROM docs WHERE doc ->> 'kind' = 'b'`)
  73. if res.RowCount != 1 || res.Rows[0][0] != int64(2) {
  74. t.Fatalf("WHERE doc ->> 'kind' = 'b' rows = %v, want id 2", res.Rows)
  75. }
  76. res = execMust(t, e, `SELECT doc ->> 'kind' AS kind FROM docs ORDER BY doc -> 'n' DESC`)
  77. if res.RowCount != 3 {
  78. t.Fatalf("ORDER BY rows = %d, want 3", res.RowCount)
  79. }
  80. if res.Rows[0][0] != "c" || res.Rows[1][0] != "b" || res.Rows[2][0] != "a" {
  81. t.Fatalf("ORDER BY doc -> 'n' DESC = %v, want c, b, a", res.Rows)
  82. }
  83. execMust(t, e, `UPDATE docs SET v = doc ->> 'kind'`)
  84. res = execMust(t, e, `SELECT id, v FROM docs ORDER BY id`)
  85. for i, want := range []string{"a", "b", "c"} {
  86. if res.Rows[i][1] != want {
  87. t.Fatalf("row %d v = %v, want %q", i, res.Rows[i][1], want)
  88. }
  89. }
  90. }
  91. // The -> operator returns JSON text (with the engine's internal JSON subtype),
  92. // not a plain string. Numeric, boolean, and comparison coercions must still
  93. // read the text form, exactly as they do for SQL TEXT; otherwise arithmetic
  94. // silently treats every extracted number as 0.
  95. func TestJSONArrowOperatorValueCoercions(t *testing.T) {
  96. _, schema, table := newTestDB(t)
  97. e := newExec(schema, table)
  98. execMust(t, e, "CREATE TABLE metrics (id INTEGER PRIMARY KEY, doc TEXT)")
  99. execMust(t, e, `INSERT INTO metrics VALUES (1, '{"n":2,"big":10,"flag":1}')`)
  100. execMust(t, e, `INSERT INTO metrics VALUES (2, '{"n":10,"big":20,"flag":0}')`)
  101. // Arithmetic uses the text numeric value. Like '2' + 1 elsewhere in the
  102. // engine, the result is REAL rather than INTEGER for JSON text.
  103. res := execMust(t, e, `SELECT (doc -> 'n') + 1 FROM metrics WHERE id = 1`)
  104. if got := res.Rows[0][0]; got != float64(3) {
  105. t.Fatalf("(doc -> 'n') + 1 = %v (%T), want 3", got, got)
  106. }
  107. // Bitwise operators coerce through toInt64.
  108. res = execMust(t, e, `SELECT (doc -> 'n') & 3 FROM metrics WHERE id = 1`)
  109. if got := res.Rows[0][0]; got != int64(2) {
  110. t.Fatalf("(doc -> 'n') & 3 = %v (%T), want int64 2", got, got)
  111. }
  112. // Numeric comparison must not fall back to string comparison, where
  113. // "10" < "9" and multi-digit values order incorrectly.
  114. res = execMust(t, e, `SELECT id FROM metrics WHERE doc -> 'big' > 9 ORDER BY id`)
  115. if res.RowCount != 2 || res.Rows[0][0] != int64(1) || res.Rows[1][0] != int64(2) {
  116. t.Fatalf("WHERE doc -> 'big' > 9 rows = %v, want ids 1, 2", res.Rows)
  117. }
  118. res = execMust(t, e, `SELECT id FROM metrics WHERE doc -> 'big' = 10`)
  119. if res.RowCount != 1 || res.Rows[0][0] != int64(1) {
  120. t.Fatalf("WHERE doc -> 'big' = 10 rows = %v, want id 1", res.Rows)
  121. }
  122. // CAST goes through toFloat.
  123. res = execMust(t, e, `SELECT CAST(doc -> 'big' AS INTEGER) FROM metrics WHERE id = 1`)
  124. if got := res.Rows[0][0]; got != int64(10) {
  125. t.Fatalf("CAST(doc -> 'big' AS INTEGER) = %v (%T), want int64 10", got, got)
  126. }
  127. // WHERE truthiness goes through toBool.
  128. res = execMust(t, e, `SELECT id FROM metrics WHERE doc -> 'flag'`)
  129. if res.RowCount != 1 || res.Rows[0][0] != int64(1) {
  130. t.Fatalf("WHERE doc -> 'flag' rows = %v, want id 1", res.Rows)
  131. }
  132. // ORDER BY compares the extracted values numerically.
  133. res = execMust(t, e, `SELECT id FROM metrics ORDER BY doc -> 'n'`)
  134. if res.RowCount != 2 || res.Rows[0][0] != int64(1) || res.Rows[1][0] != int64(2) {
  135. t.Fatalf("ORDER BY doc -> 'n' rows = %v, want ids 1, 2", res.Rows)
  136. }
  137. // The JSON subtype must survive -> so JSON functions embed the extracted
  138. // value as JSON rather than as a quoted string.
  139. res = execMust(t, e, `SELECT json_array(doc -> 'n') FROM metrics WHERE id = 1`)
  140. if got := res.Rows[0][0]; got != "[2]" {
  141. t.Fatalf("json_array(doc -> 'n') = %v, want [2]", got)
  142. }
  143. }