arrays_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. package executor
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  7. "github.com/danfragoso/pizzasql-next/pkg/storage"
  8. )
  9. func TestArrayColumnDDLAndRoundTrip(t *testing.T) {
  10. _, schema, table := newTestDB(t)
  11. e := newExec(schema, table)
  12. execMust(t, e, `CREATE TABLE items (
  13. id INTEGER PRIMARY KEY,
  14. tags TEXT[] NOT NULL,
  15. scores INTEGER[],
  16. flags BOOLEAN[],
  17. ratios DOUBLE PRECISION[]
  18. )`)
  19. execMust(t, e, `INSERT INTO items (id, tags, scores, flags, ratios)
  20. VALUES (1, '{a,b}', '{1,2}', '{t,f}', '{1.5,2.25}')`)
  21. execMust(t, e, `INSERT INTO items (id, tags, scores)
  22. VALUES (2, '{}', NULL)`)
  23. res := execMust(t, e, `SELECT id, tags, scores, flags, ratios FROM items ORDER BY id`)
  24. if got := res.Rows[0][1]; got != "{a,b}" {
  25. t.Fatalf("tags = %v, want {a,b}", got)
  26. }
  27. // Elements are canonicalized per declared type.
  28. if got := res.Rows[0][2]; got != "{1,2}" {
  29. t.Fatalf("scores = %v, want {1,2}", got)
  30. }
  31. if got := res.Rows[0][3]; got != "{t,f}" {
  32. t.Fatalf("flags = %v, want {t,f}", got)
  33. }
  34. if got := res.Rows[0][4]; got != "{1.5,2.25}" {
  35. t.Fatalf("ratios = %v, want {1.5,2.25}", got)
  36. }
  37. // NULL and the empty array stay distinguishable.
  38. if got := res.Rows[1][1]; got != "{}" {
  39. t.Fatalf("empty tags = %v, want {}", got)
  40. }
  41. if got := res.Rows[1][2]; got != nil {
  42. t.Fatalf("NULL scores = %v, want nil", got)
  43. }
  44. // Result metadata reports the declared array type.
  45. if got := res.ColumnTypes[1]; got != "TEXT[]" {
  46. t.Fatalf("tags column type = %q, want TEXT[]", got)
  47. }
  48. // The durable catalog keeps the array spelling, so sqlite_master and
  49. // PRAGMA table_info still render it.
  50. master := execMust(t, e, `SELECT sql FROM sqlite_master WHERE name = 'items'`)
  51. if sql, _ := master.Rows[0][0].(string); !strings.Contains(sql, "TEXT[]") {
  52. t.Fatalf("sqlite_master SQL = %q, want it to contain TEXT[]", sql)
  53. }
  54. info := execMust(t, e, `PRAGMA table_info(items)`)
  55. found := false
  56. for _, row := range info.Rows {
  57. if row[1] == "scores" {
  58. found = true
  59. if row[2] != "INTEGER[]" {
  60. t.Fatalf("PRAGMA table_info type = %v, want INTEGER[]", row[2])
  61. }
  62. }
  63. }
  64. if !found {
  65. t.Fatal("PRAGMA table_info did not report the scores column")
  66. }
  67. // A fresh executor over the same store sees the same bytes: no reload
  68. // conversion and no storage-format change.
  69. reloaded := newExec(schema, table)
  70. res = execMust(t, reloaded, `SELECT tags, scores, flags FROM items WHERE id = 1`)
  71. if res.Rows[0][0] != "{a,b}" || res.Rows[0][1] != "{1,2}" || res.Rows[0][2] != "{t,f}" {
  72. t.Fatalf("reloaded row = %v", res.Rows[0])
  73. }
  74. }
  75. func TestArrayCanonicalEncoding(t *testing.T) {
  76. _, schema, table := newTestDB(t)
  77. e := newExec(schema, table)
  78. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT[])`)
  79. for _, tc := range []struct{ sql, want string }{
  80. {`INSERT INTO t VALUES (1, '{ a , b }') RETURNING v`, "{a,b}"},
  81. {`INSERT INTO t VALUES (2, '{"a b","c,d"}') RETURNING v`, `{"a b","c,d"}`},
  82. {`INSERT INTO t VALUES (3, ARRAY['x', NULL, 'NULL']) RETURNING v`, `{x,NULL,"NULL"}`},
  83. {`INSERT INTO t VALUES (4, ARRAY[]::TEXT[]) RETURNING v`, "{}"},
  84. {`INSERT INTO t VALUES (5, '{"quote\"d", "back\\slash"}') RETURNING v`, `{"quote\"d","back\\slash"}`},
  85. } {
  86. res := execMust(t, e, tc.sql)
  87. if got := res.Rows[0][0]; got != tc.want {
  88. t.Errorf("%s = %v, want %s", tc.sql, got, tc.want)
  89. }
  90. }
  91. }
  92. func TestArrayConstructorAndCast(t *testing.T) {
  93. _, schema, table := newTestDB(t)
  94. e := newExec(schema, table)
  95. cases := []struct {
  96. sql string
  97. want interface{}
  98. }{
  99. {`SELECT ARRAY[1, 2, 3]`, "{1,2,3}"},
  100. {`SELECT ARRAY['a', NULL]`, "{a,NULL}"},
  101. {`SELECT ARRAY[TRUE, FALSE]`, "{t,f}"},
  102. {`SELECT '{1,2,3}'::INTEGER[]`, "{1,2,3}"},
  103. {`SELECT CAST('{1,2}' AS INTEGER[])`, "{1,2}"},
  104. {`SELECT ARRAY['1','2']::INTEGER[]`, "{1,2}"},
  105. {`SELECT '{1.50,2}'::NUMERIC[]`, "{1.5,2}"},
  106. {`SELECT NULL::INTEGER[]`, nil},
  107. }
  108. for _, tc := range cases {
  109. res := execMust(t, e, tc.sql)
  110. if got := res.Rows[0][0]; got != tc.want {
  111. t.Errorf("%s = %#v, want %#v", tc.sql, got, tc.want)
  112. }
  113. }
  114. res := execMust(t, e, `SELECT ARRAY[1,2] AS a, '{x}'::TEXT[] AS b`)
  115. if res.ColumnTypes[0] != "INTEGER[]" || res.ColumnTypes[1] != "TEXT[]" {
  116. t.Fatalf("constructor types = %v, want [INTEGER[] TEXT[]]", res.ColumnTypes)
  117. }
  118. }
  119. func TestArraySubscriptAndLength(t *testing.T) {
  120. _, schema, table := newTestDB(t)
  121. e := newExec(schema, table)
  122. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, nums INTEGER[], tags TEXT[], flags BOOLEAN[])`)
  123. execMust(t, e, `INSERT INTO t VALUES (1, '{10,20,30}', '{"x", NULL, "z"}', '{t,f}')`)
  124. execMust(t, e, `INSERT INTO t VALUES (2, '{}', '{}', '{}')`)
  125. cases := []struct {
  126. sql string
  127. want interface{}
  128. }{
  129. {`SELECT nums[1] FROM t WHERE id = 1`, "10"},
  130. {`SELECT nums[3] FROM t WHERE id = 1`, "30"},
  131. {`SELECT nums[4] FROM t WHERE id = 1`, nil},
  132. {`SELECT nums[0] FROM t WHERE id = 1`, nil},
  133. {`SELECT tags[2] FROM t WHERE id = 1`, nil},
  134. {`SELECT tags[1] FROM t WHERE id = 1`, "x"},
  135. {`SELECT flags[1] FROM t WHERE id = 1`, "t"},
  136. {`SELECT nums[1] FROM t WHERE id = 2`, nil},
  137. {`SELECT array_length(nums, 1) FROM t WHERE id = 1`, int64(3)},
  138. {`SELECT array_length(nums, 1) FROM t WHERE id = 2`, nil},
  139. {`SELECT array_length(nums, 2) FROM t WHERE id = 1`, nil},
  140. }
  141. for _, tc := range cases {
  142. res := execMust(t, e, tc.sql)
  143. if got := res.Rows[0][0]; got != tc.want {
  144. t.Errorf("%s = %#v, want %#v", tc.sql, got, tc.want)
  145. }
  146. }
  147. res := execMust(t, e, `SELECT nums[1] AS n FROM t WHERE id = 1`)
  148. if res.ColumnTypes[0] != "INTEGER" {
  149. t.Fatalf("subscript type = %q, want INTEGER", res.ColumnTypes[0])
  150. }
  151. }
  152. func TestAnyAllThreeValuedLogic(t *testing.T) {
  153. _, schema, table := newTestDB(t)
  154. e := newExec(schema, table)
  155. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER, b BOOLEAN)`)
  156. execMust(t, e, `INSERT INTO t VALUES (1, 2, TRUE)`)
  157. cases := []struct {
  158. sql string
  159. want interface{}
  160. }{
  161. // ANY: true if any comparison is true.
  162. {`SELECT 2 = ANY('{1,2,3}')`, true},
  163. {`SELECT 5 = ANY('{1,2,3}')`, false},
  164. // NULL elements: no true and a NULL comparison yields NULL.
  165. {`SELECT 5 = ANY('{1,NULL,3}')`, nil},
  166. {`SELECT 2 = ANY('{NULL,2}')`, true},
  167. // Empty array: ANY is false, ALL is true, even for a NULL left side.
  168. {`SELECT 5 = ANY('{}')`, false},
  169. {`SELECT NULL = ANY('{}')`, false},
  170. {`SELECT 5 <> ALL('{}')`, true},
  171. {`SELECT NULL <> ALL('{}')`, true},
  172. // ALL: false if any comparison is false, NULL if only NULLs remain.
  173. {`SELECT 2 <> ALL('{1,3,4}')`, true},
  174. {`SELECT 2 <> ALL('{1,2,4}')`, false},
  175. {`SELECT 2 <> ALL('{1,NULL}')`, nil},
  176. {`SELECT 3 > ALL('{1,2}')`, true},
  177. {`SELECT 1 >= ALL('{1,NULL}')`, nil},
  178. // A NULL left operand yields NULL for a non-empty array.
  179. {`SELECT NULL = ANY('{1,2}')`, nil},
  180. {`SELECT NULL <> ALL('{1,2}')`, nil},
  181. // Boolean elements compare as booleans, not as text.
  182. {`SELECT TRUE = ANY('{f,t}')`, true},
  183. {`SELECT FALSE = ANY('{f,t}')`, true},
  184. {`SELECT TRUE <> ALL('{f,t}')`, false},
  185. // Column-driven forms, including a NULL column.
  186. {`SELECT n = ANY('{1,2}') FROM t`, true},
  187. {`SELECT n <> ALL('{1,2}') FROM t`, false},
  188. {`SELECT b = ANY('{t}') FROM t`, true},
  189. {`SELECT id = ANY('{9,8}') FROM t`, false},
  190. }
  191. for _, tc := range cases {
  192. res := execMust(t, e, tc.sql)
  193. if got := res.Rows[0][0]; got != tc.want {
  194. t.Errorf("%s = %#v, want %#v", tc.sql, got, tc.want)
  195. }
  196. }
  197. if res := execMust(t, e, `SELECT n = ANY('{1,2}') AS hit FROM t`); res.ColumnTypes[0] != "BOOLEAN" {
  198. t.Fatalf("ANY result type = %q, want BOOLEAN", res.ColumnTypes[0])
  199. }
  200. }
  201. func TestAnyAllOverArrayColumn(t *testing.T) {
  202. _, schema, table := newTestDB(t)
  203. e := newExec(schema, table)
  204. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, allowed TEXT[])`)
  205. execMust(t, e, `INSERT INTO t VALUES (1, '{red,green}'), (2, '{blue}'), (3, NULL)`)
  206. for _, tc := range []struct {
  207. sql string
  208. want int
  209. }{
  210. {`SELECT count(*) FROM t WHERE 'red' = ANY(allowed)`, 1},
  211. {`SELECT count(*) FROM t WHERE 'purple' = ANY(allowed)`, 0},
  212. {`SELECT count(*) FROM t WHERE 'purple' <> ALL(allowed)`, 2},
  213. } {
  214. res := execMust(t, e, tc.sql)
  215. if got := res.Rows[0][0]; got != int64(tc.want) {
  216. t.Errorf("%s = %v, want %d", tc.sql, got, tc.want)
  217. }
  218. }
  219. }
  220. func TestUnnestAsTableFunction(t *testing.T) {
  221. _, schema, table := newTestDB(t)
  222. e := newExec(schema, table)
  223. res := execMust(t, e, `SELECT * FROM unnest('{10,20,30}'::INTEGER[])`)
  224. if res.RowCount != 3 {
  225. t.Fatalf("unnest rows = %d, want 3", res.RowCount)
  226. }
  227. if res.Columns[0] != "unnest" {
  228. t.Fatalf("unnest column = %q, want unnest", res.Columns[0])
  229. }
  230. if res.Rows[0][0] != int64(10) || res.Rows[2][0] != int64(30) {
  231. t.Fatalf("unnest values = %v", res.Rows)
  232. }
  233. if res.ColumnTypes[0] != "INTEGER" {
  234. t.Fatalf("unnest column type = %q, want INTEGER", res.ColumnTypes[0])
  235. }
  236. res = execMust(t, e, `SELECT u.unnest FROM unnest(ARRAY['a', 'b']) AS u WHERE u.unnest <> 'a'`)
  237. if res.RowCount != 1 || res.Rows[0][0] != "b" {
  238. t.Fatalf("unnest alias rows = %v", res.Rows)
  239. }
  240. res = execMust(t, e, `SELECT * FROM unnest(NULL::TEXT[])`)
  241. if res.RowCount != 0 {
  242. t.Fatalf("unnest(NULL) rows = %d, want 0", res.RowCount)
  243. }
  244. if _, err := execSQL(e, `SELECT unnest('{1,2}'::INTEGER[])`); err == nil {
  245. t.Fatal("scalar unnest should be rejected")
  246. }
  247. }
  248. func TestArrayDefaultsAndAlterTable(t *testing.T) {
  249. _, schema, table := newTestDB(t)
  250. e := newExec(schema, table)
  251. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, tags TEXT[] DEFAULT ARRAY['a'])`)
  252. execMust(t, e, `INSERT INTO t (id) VALUES (1)`)
  253. res := execMust(t, e, `SELECT tags FROM t WHERE id = 1`)
  254. if res.Rows[0][0] != "{a}" {
  255. t.Fatalf("default tags = %v, want {a}", res.Rows[0][0])
  256. }
  257. execMust(t, e, `ALTER TABLE t ADD COLUMN nums INTEGER[] DEFAULT '{1, 2}'`)
  258. res = execMust(t, e, `SELECT nums FROM t WHERE id = 1`)
  259. if res.Rows[0][0] != "{1,2}" {
  260. t.Fatalf("added column default = %v, want {1,2}", res.Rows[0][0])
  261. }
  262. execMust(t, e, `INSERT INTO t (id, nums) VALUES (2, '{3}')`)
  263. res = execMust(t, e, `SELECT nums FROM t WHERE id = 2`)
  264. if res.Rows[0][0] != "{3}" {
  265. t.Fatalf("explicit array = %v, want {3}", res.Rows[0][0])
  266. }
  267. }
  268. func TestArrayInCompositePrimaryKey(t *testing.T) {
  269. _, schema, table := newTestDB(t)
  270. e := newExec(schema, table)
  271. execMust(t, e, `CREATE TABLE t (id INTEGER, tags TEXT[], PRIMARY KEY (id, tags))`)
  272. execMust(t, e, `INSERT INTO t VALUES (1, '{a,b}')`)
  273. execMust(t, e, `INSERT INTO t VALUES (1, '{c}')`)
  274. res := execMust(t, e, `SELECT count(*) FROM t`)
  275. if res.Rows[0][0] != int64(2) {
  276. t.Fatalf("rows = %v, want 2", res.Rows[0][0])
  277. }
  278. if _, err := execSQL(e, `INSERT INTO t VALUES (1, '{a, b}')`); err == nil {
  279. t.Fatal("duplicate composite key with an equivalent array should conflict")
  280. }
  281. reloaded := newExec(schema, table)
  282. res = execMust(t, reloaded, `SELECT tags FROM t WHERE id = 1 AND tags = '{a,b}'`)
  283. if res.RowCount != 1 || res.Rows[0][0] != "{a,b}" {
  284. t.Fatalf("reloaded composite-key lookup = %v", res.Rows)
  285. }
  286. }
  287. func TestArrayRejectedShapes(t *testing.T) {
  288. _, schema, table := newTestDB(t)
  289. e := newExec(schema, table)
  290. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT[])`)
  291. cases := []struct {
  292. sql string
  293. unsupported bool
  294. }{
  295. {`INSERT INTO t VALUES (1, '{{1,2},{3,4}}')`, true},
  296. {`SELECT '{{1,2},{3,4}}'::INTEGER[]`, true},
  297. {`SELECT 1 = ANY('not an array')`, false},
  298. {`SELECT 1 = ANY(1)`, false},
  299. {`INSERT INTO t VALUES (2, '{1,2')`, false},
  300. {`INSERT INTO t VALUES (3, 5)`, false},
  301. }
  302. for _, tc := range cases {
  303. _, err := execSQL(e, tc.sql)
  304. if err == nil {
  305. t.Errorf("%s succeeded, want an error", tc.sql)
  306. continue
  307. }
  308. if tc.unsupported && !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  309. t.Errorf("%s error = %v, want ErrFeatureNotSupported", tc.sql, err)
  310. }
  311. if !tc.unsupported && !errors.Is(err, pgerror.ErrInvalidText) {
  312. t.Errorf("%s error = %v, want ErrInvalidText", tc.sql, err)
  313. }
  314. }
  315. }
  316. func TestArrayUpdateAndReturning(t *testing.T) {
  317. _, schema, table := newTestDB(t)
  318. e := newExec(schema, table)
  319. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, tags TEXT[])`)
  320. execMust(t, e, `INSERT INTO t VALUES (1, '{a}')`)
  321. res := execMust(t, e, `UPDATE t SET tags = '{b, c}' WHERE id = 1 RETURNING tags`)
  322. if res.Rows[0][0] != "{b,c}" {
  323. t.Fatalf("UPDATE RETURNING tags = %v, want {b,c}", res.Rows[0][0])
  324. }
  325. res = execMust(t, e, `SELECT tags FROM t WHERE id = 1`)
  326. if res.Rows[0][0] != "{b,c}" {
  327. t.Fatalf("stored tags = %v, want {b,c}", res.Rows[0][0])
  328. }
  329. }
  330. // The array value is a plain string in the row codec, so a second decoder sees
  331. // exactly the bytes the first one wrote. A freshly constructed schema and table
  332. // manager pair over the same store stands in for a process restart: no
  333. // converter runs, and the durable catalog plus row bytes are read as written.
  334. func TestArrayStorageReloadIsStable(t *testing.T) {
  335. pool, schema, table := newTestDB(t)
  336. e := newExec(schema, table)
  337. execMust(t, e, `CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT[], n INTEGER[])`)
  338. execMust(t, e, `INSERT INTO t VALUES (1, ARRAY['x','y'], '{1,2}')`)
  339. session := storage.NewSession(schema, table)
  340. row, err := session.GetByPK("t", "1")
  341. if err != nil {
  342. t.Fatalf("GetByPK: %v", err)
  343. }
  344. if row["v"] != "{x,y}" {
  345. t.Fatalf("stored value = %#v, want {x,y}", row["v"])
  346. }
  347. reloadedSchema := storage.NewSchemaManager(pool, "testdb")
  348. reloadedTable := storage.NewTableManager(pool, reloadedSchema, "testdb")
  349. reloaded := newExec(reloadedSchema, reloadedTable)
  350. res := execMust(t, reloaded, `SELECT v, n FROM t WHERE id = 1`)
  351. if res.Rows[0][0] != "{x,y}" || res.Rows[0][1] != "{1,2}" {
  352. t.Fatalf("reloaded row = %v, want [{x,y} {1,2}]", res.Rows[0])
  353. }
  354. if res.ColumnTypes[0] != "TEXT[]" || res.ColumnTypes[1] != "INTEGER[]" {
  355. t.Fatalf("reloaded types = %v, want [TEXT[] INTEGER[]]", res.ColumnTypes)
  356. }
  357. // A second write through the reloaded session must encode identically.
  358. execMust(t, reloaded, `INSERT INTO t (id, v) VALUES (2, '{x, y}')`)
  359. res = execMust(t, reloaded, `SELECT v = '{x,y}' FROM t WHERE id = 2`)
  360. if res.Rows[0][0] != true {
  361. t.Fatalf("canonical reloaded write did not compare equal: %v", res.Rows[0][0])
  362. }
  363. }