2
0

features_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package executor
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/storage"
  7. )
  8. func TestInsertReturningGeneratedIDAndProjections(t *testing.T) {
  9. _, schema, table := newTestDB(t)
  10. e := newExec(schema, table)
  11. execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)")
  12. res := execMust(t, e, "INSERT INTO users (name) VALUES ('alice') RETURNING id, name, id + 1 AS next_id")
  13. if len(res.Columns) != 3 {
  14. t.Fatalf("expected 3 columns, got %v", res.Columns)
  15. }
  16. if res.Columns[0] != "id" || res.Columns[1] != "name" || res.Columns[2] != "next_id" {
  17. t.Fatalf("unexpected columns %v", res.Columns)
  18. }
  19. if res.RowCount != 1 {
  20. t.Fatalf("expected 1 row, got %d", res.RowCount)
  21. }
  22. if res.Rows[0][0] != int64(1) || res.Rows[0][1] != "alice" || res.Rows[0][2] != int64(2) {
  23. t.Fatalf("unexpected row %v", res.Rows[0])
  24. }
  25. if res.LastInsertID != 1 {
  26. t.Fatalf("expected LastInsertID 1, got %d", res.LastInsertID)
  27. }
  28. }
  29. func TestInsertReturningStar(t *testing.T) {
  30. _, schema, table := newTestDB(t)
  31. e := newExec(schema, table)
  32. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  33. res := execMust(t, e, "INSERT INTO t (v) VALUES ('x') RETURNING *")
  34. if res.RowCount != 1 || len(res.Columns) != 2 {
  35. t.Fatalf("unexpected result %v %v", res.Columns, res.Rows)
  36. }
  37. if res.Rows[0][1] != "x" {
  38. t.Fatalf("unexpected row %v", res.Rows[0])
  39. }
  40. }
  41. func TestUpdateReturning(t *testing.T) {
  42. _, schema, table := newTestDB(t)
  43. e := newExec(schema, table)
  44. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  45. execMust(t, e, "INSERT INTO t VALUES (1, 'a'), (2, 'b')")
  46. res := execMust(t, e, "UPDATE t SET v = upper(v) RETURNING id, v")
  47. if res.RowCount != 2 {
  48. t.Fatalf("expected 2 rows, got %d", res.RowCount)
  49. }
  50. got := map[interface{}]interface{}{}
  51. for _, row := range res.Rows {
  52. got[row[0]] = row[1]
  53. }
  54. if got[int64(1)] != "A" || got[int64(2)] != "B" {
  55. t.Fatalf("unexpected returning rows %v", res.Rows)
  56. }
  57. }
  58. func TestDeleteReturning(t *testing.T) {
  59. _, schema, table := newTestDB(t)
  60. e := newExec(schema, table)
  61. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  62. execMust(t, e, "INSERT INTO t VALUES (1, 'a'), (2, 'b')")
  63. res := execMust(t, e, "DELETE FROM t WHERE id = 1 RETURNING id, v")
  64. if res.RowCount != 1 || res.Rows[0][0] != int64(1) || res.Rows[0][1] != "a" {
  65. t.Fatalf("unexpected returning rows %v", res.Rows)
  66. }
  67. if res.RowsAffected != 1 {
  68. t.Fatalf("expected RowsAffected 1, got %d", res.RowsAffected)
  69. }
  70. }
  71. func TestSQLiteVersionDistinctFromPizzasqlVersion(t *testing.T) {
  72. _, schema, table := newTestDB(t)
  73. e := newExec(schema, table)
  74. sqlite := execMust(t, e, "SELECT sqlite_version()")
  75. psql := execMust(t, e, "SELECT pizzasql_version()")
  76. if sqlite.Rows[0][0] != SQLiteCompatVersion {
  77. t.Fatalf("sqlite_version = %v, want %s", sqlite.Rows[0][0], SQLiteCompatVersion)
  78. }
  79. if SQLiteCompatVersion < "3.35.0" {
  80. t.Fatalf("SQLite compatibility floor must be >= 3.35.0, got %s", SQLiteCompatVersion)
  81. }
  82. if sqlite.Rows[0][0] == psql.Rows[0][0] {
  83. t.Fatalf("sqlite_version and pizzasql_version must differ")
  84. }
  85. }
  86. func TestGeneratedStoredColumnRecompute(t *testing.T) {
  87. _, schema, table := newTestDB(t)
  88. e := newExec(schema, table)
  89. execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER, total INTEGER GENERATED ALWAYS AS (a + b) STORED)")
  90. execMust(t, e, "INSERT INTO t (a, b) VALUES (2, 3)")
  91. res := execMust(t, e, "SELECT total FROM t")
  92. if res.Rows[0][0] != int64(5) {
  93. t.Fatalf("generated total = %v, want 5", res.Rows[0][0])
  94. }
  95. execMust(t, e, "UPDATE t SET a = 10")
  96. res = execMust(t, e, "SELECT total FROM t")
  97. if res.Rows[0][0] != int64(13) {
  98. t.Fatalf("generated total after update = %v, want 13", res.Rows[0][0])
  99. }
  100. }
  101. func TestGeneratedColumnReferencesAutoIncrementID(t *testing.T) {
  102. _, schema, table := newTestDB(t)
  103. e := newExec(schema, table)
  104. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, stored INTEGER GENERATED ALWAYS AS (id + 1) STORED)")
  105. res := execMust(t, e, "INSERT INTO t (id) VALUES (NULL) RETURNING id, stored")
  106. if res.Rows[0][0] != int64(1) || res.Rows[0][1] != int64(2) {
  107. t.Fatalf("generated id reference = %v, want [1 2]", res.Rows[0])
  108. }
  109. res = execMust(t, e, "SELECT stored FROM t WHERE id = 1")
  110. if res.Rows[0][0] != int64(2) {
  111. t.Fatalf("persisted generated value = %v, want 2", res.Rows[0][0])
  112. }
  113. }
  114. func TestGeneratedColumnRejectsUserWrites(t *testing.T) {
  115. _, schema, table := newTestDB(t)
  116. e := newExec(schema, table)
  117. execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a + 1) STORED)")
  118. if _, err := execSQL(e, "INSERT INTO t (a, b) VALUES (1, 99)"); err == nil {
  119. t.Fatal("expected explicit insert into generated column to fail")
  120. }
  121. if _, err := execSQL(e, "UPDATE t SET b = 5"); err == nil {
  122. t.Fatal("expected update of generated column to fail")
  123. }
  124. }
  125. func TestInsertOrReplacePrimaryKey(t *testing.T) {
  126. _, schema, table := newTestDB(t)
  127. e := newExec(schema, table)
  128. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  129. execMust(t, e, "INSERT INTO t VALUES (1, 'a')")
  130. execMust(t, e, "INSERT OR REPLACE INTO t VALUES (1, 'b')")
  131. res := execMust(t, e, "SELECT v FROM t")
  132. if res.RowCount != 1 || res.Rows[0][0] != "b" {
  133. t.Fatalf("INSERT OR REPLACE result = %v", res.Rows)
  134. }
  135. }
  136. func TestTableUniqueOnConflictReplace(t *testing.T) {
  137. _, schema, table := newTestDB(t)
  138. e := newExec(schema, table)
  139. execMust(t, e, `CREATE TABLE t (
  140. site_id INTEGER,
  141. path TEXT,
  142. total INTEGER,
  143. CONSTRAINT "t#site#path" UNIQUE(site_id, path) ON CONFLICT REPLACE
  144. )`)
  145. execMust(t, e, "INSERT INTO t (site_id, path, total) VALUES (1, '/a', 10)")
  146. execMust(t, e, "INSERT INTO t (site_id, path, total) VALUES (1, '/a', 42)")
  147. res := execMust(t, e, "SELECT total FROM t WHERE site_id = 1 AND path = '/a'")
  148. if res.RowCount != 1 {
  149. t.Fatalf("expected 1 row after replace, got %d", res.RowCount)
  150. }
  151. if res.Rows[0][0] != int64(42) {
  152. t.Fatalf("expected replaced total 42, got %v", res.Rows[0][0])
  153. }
  154. }
  155. func TestInsertIgnoreUniqueIndex(t *testing.T) {
  156. _, schema, table := newTestDB(t)
  157. e := newExec(schema, table)
  158. execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
  159. execMust(t, e, "INSERT INTO users VALUES (1, 'same@example.com')")
  160. res := execMust(t, e, "INSERT OR IGNORE INTO users VALUES (2, 'same@example.com')")
  161. if res.RowsAffected != 0 {
  162. t.Fatalf("INSERT OR IGNORE affected %d rows, want 0", res.RowsAffected)
  163. }
  164. res = execMust(t, e, "INSERT INTO users VALUES (3, 'same@example.com') ON CONFLICT DO NOTHING")
  165. if res.RowsAffected != 0 {
  166. t.Fatalf("targetless DO NOTHING affected %d rows, want 0", res.RowsAffected)
  167. }
  168. res = execMust(t, e, "SELECT id FROM users")
  169. if res.RowCount != 1 || res.Rows[0][0] != int64(1) {
  170. t.Fatalf("users = %v, want only id 1", res.Rows)
  171. }
  172. execMust(t, e, "INSERT INTO users VALUES (4, NULL)")
  173. execMust(t, e, "INSERT OR IGNORE INTO users VALUES (5, NULL)")
  174. execMust(t, e, "INSERT INTO users VALUES (6, NULL) ON CONFLICT DO NOTHING")
  175. res = execMust(t, e, "SELECT count(*) FROM users WHERE email IS NULL")
  176. if res.Rows[0][0] != int64(3) {
  177. t.Fatalf("NULL unique values = %v, want 3 rows", res.Rows[0][0])
  178. }
  179. }
  180. func TestJSON1Functions(t *testing.T) {
  181. _, schema, table := newTestDB(t)
  182. e := newExec(schema, table)
  183. res := execMust(t, e, `SELECT json_extract('{"a": 1, "b": [10, 20]}', '$.b[1]')`)
  184. if res.Rows[0][0] != int64(20) {
  185. t.Fatalf("json_extract = %v, want 20", res.Rows[0][0])
  186. }
  187. res = execMust(t, e, `SELECT json_set('{"a": 1}', '$.a', 2)`)
  188. if res.Rows[0][0] != `{"a":2}` {
  189. t.Fatalf("json_set = %v", res.Rows[0][0])
  190. }
  191. // The JSON subtype must survive nested calls, matching GoatCounter's
  192. // json_insert(json_extract(...), '$[#]', json(...)) pattern.
  193. res = execMust(t, e, `SELECT json_insert(json_extract('{"w":[]}', '$.w'), '$[#]', json('{"n":"languages"}'))`)
  194. if res.Rows[0][0] != `[{"n":"languages"}]` {
  195. t.Fatalf("json_insert with json() = %v", res.Rows[0][0])
  196. }
  197. res = execMust(t, e, `SELECT json_replace('{"collect": 1}', '$.collect', json_extract('{"collect": 1}', '$.collect') | 64)`)
  198. if res.Rows[0][0] != `{"collect":65}` {
  199. t.Fatalf("json_replace with bitwise = %v", res.Rows[0][0])
  200. }
  201. res = execMust(t, e, `SELECT json_group_array(x) FROM (SELECT 1 AS x UNION ALL SELECT 2 UNION ALL SELECT 3) AS t`)
  202. if res.Rows[0][0] != `[1,2,3]` {
  203. t.Fatalf("json_group_array = %v", res.Rows[0][0])
  204. }
  205. }
  206. func TestBitwiseOperators(t *testing.T) {
  207. _, schema, table := newTestDB(t)
  208. e := newExec(schema, table)
  209. cases := map[string]int64{
  210. "SELECT 6 & 3": 2,
  211. "SELECT 6 | 1": 7,
  212. "SELECT 1 << 4": 16,
  213. "SELECT 32 >> 2": 8,
  214. "SELECT ~0": -1,
  215. "SELECT 1 + 2 | 4": 7, // (1+2)|4
  216. "SELECT 2 | 1 * 8": 10,
  217. }
  218. for sql, want := range cases {
  219. res := execMust(t, e, sql)
  220. if res.Rows[0][0] != want {
  221. t.Errorf("%s = %v, want %d", sql, res.Rows[0][0], want)
  222. }
  223. }
  224. }
  225. func TestPercentDiff(t *testing.T) {
  226. _, schema, table := newTestDB(t)
  227. e := newExec(schema, table)
  228. res := execMust(t, e, "SELECT percent_diff(10, 15)")
  229. if res.Rows[0][0] != float64(50) {
  230. t.Fatalf("percent_diff(10,15) = %v, want 50", res.Rows[0][0])
  231. }
  232. res = execMust(t, e, "SELECT percent_diff(0, 5)")
  233. if f, ok := res.Rows[0][0].(float64); !ok || f <= 0 {
  234. t.Fatalf("percent_diff(0,5) should be +Inf, got %v", res.Rows[0][0])
  235. }
  236. res = execMust(t, e, "SELECT percent_diff(NULL, 5)")
  237. if res.Rows[0][0] != nil {
  238. t.Fatalf("percent_diff(NULL,5) should be NULL, got %v", res.Rows[0][0])
  239. }
  240. }
  241. func TestBlobLiteralStorageAndFunctions(t *testing.T) {
  242. _, schema, table := newTestDB(t)
  243. e := newExec(schema, table)
  244. execMust(t, e, "CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)")
  245. execMust(t, e, "INSERT INTO blobs (id, data) VALUES (1, X'00FF10')")
  246. res := execMust(t, e, "SELECT data FROM blobs WHERE id = 1")
  247. b, ok := res.Rows[0][0].([]byte)
  248. if !ok || len(b) != 3 || b[0] != 0x00 || b[1] != 0xFF || b[2] != 0x10 {
  249. t.Fatalf("blob round-trip = %#v", res.Rows[0][0])
  250. }
  251. res = execMust(t, e, "SELECT hex(data), typeof(data) FROM blobs WHERE id = 1")
  252. if res.Rows[0][0] != "00FF10" {
  253. t.Fatalf("hex = %v", res.Rows[0][0])
  254. }
  255. if res.Rows[0][1] != "blob" {
  256. t.Fatalf("typeof = %v", res.Rows[0][1])
  257. }
  258. res = execMust(t, e, "SELECT unhex('00FF')")
  259. if b, ok := res.Rows[0][0].([]byte); !ok || len(b) != 2 || b[1] != 0xFF {
  260. t.Fatalf("unhex = %#v", res.Rows[0][0])
  261. }
  262. res = execMust(t, e, "SELECT CAST('abc' AS BLOB)")
  263. if b, ok := res.Rows[0][0].([]byte); !ok || string(b) != "abc" {
  264. t.Fatalf("cast to blob = %#v", res.Rows[0][0])
  265. }
  266. }
  267. func TestExpressionUniqueIndexLower(t *testing.T) {
  268. _, schema, table := newTestDB(t)
  269. e := newExec(schema, table)
  270. execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
  271. execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
  272. execMust(t, e, "INSERT INTO users (id, email) VALUES (1, 'Alice@Example.com')")
  273. if _, err := execSQL(e, "INSERT INTO users (id, email) VALUES (2, 'alice@example.com')"); err == nil {
  274. t.Fatal("expected expression unique index to reject a case-insensitive duplicate")
  275. }
  276. // A NULL or distinct value is still allowed.
  277. execMust(t, e, "INSERT INTO users (id, email) VALUES (3, 'bob@example.com')")
  278. }
  279. func TestExpressionUniqueIndexReplace(t *testing.T) {
  280. _, schema, table := newTestDB(t)
  281. e := newExec(schema, table)
  282. execMust(t, e, "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, tag TEXT)")
  283. execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
  284. execMust(t, e, "INSERT INTO users (id, email, tag) VALUES (1, 'Alice@Example.com', 'old')")
  285. // INSERT OR REPLACE must replace the conflicting row even though the
  286. // conflict is on a case-insensitive expression index.
  287. execMust(t, e, "INSERT OR REPLACE INTO users (id, email, tag) VALUES (2, 'alice@example.com', 'new')")
  288. res := execMust(t, e, "SELECT id, tag FROM users")
  289. if res.RowCount != 1 {
  290. t.Fatalf("expected 1 row after expression replace, got %d: %v", res.RowCount, res.Rows)
  291. }
  292. if res.Rows[0][0] != int64(2) || res.Rows[0][1] != "new" {
  293. t.Fatalf("unexpected replaced row %v", res.Rows[0])
  294. }
  295. }
  296. func TestInsertReturningMetadataTypes(t *testing.T) {
  297. _, schema, table := newTestDB(t)
  298. e := newExec(schema, table)
  299. execMust(t, e, "CREATE TABLE t (id BIGINT PRIMARY KEY, name TEXT)")
  300. res := execMust(t, e, "INSERT INTO t (id, name) VALUES (7, 'x') RETURNING id, name")
  301. if len(res.ColumnTypes) != 2 || res.ColumnTypes[0] != "BIGINT" || res.ColumnTypes[1] != "TEXT" {
  302. t.Fatalf("unexpected returning column types %v", res.ColumnTypes)
  303. }
  304. }
  305. func TestGeneratedColumnReturning(t *testing.T) {
  306. _, schema, table := newTestDB(t)
  307. e := newExec(schema, table)
  308. execMust(t, e, "CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
  309. res := execMust(t, e, "INSERT INTO t (a) VALUES (21) RETURNING a, b")
  310. if res.Rows[0][1] != int64(42) {
  311. t.Fatalf("expected generated b=42 in RETURNING, got %v", res.Rows[0][1])
  312. }
  313. }
  314. func TestGeneratedExpressionIndexText(t *testing.T) {
  315. _, schema, table := newTestDB(t)
  316. e := newExec(schema, table)
  317. execMust(t, e, "CREATE TABLE users (email TEXT)")
  318. execMust(t, e, "CREATE UNIQUE INDEX users_email_lower ON users (lower(email))")
  319. idx, err := schema.GetIndex("users_email_lower")
  320. if err != nil {
  321. t.Fatalf("GetIndex: %v", err)
  322. }
  323. if len(idx.Columns) != 1 || idx.Columns[0].Expression == "" {
  324. t.Fatalf("expected persisted expression, got %#v", idx.Columns)
  325. }
  326. if !strings.Contains(strings.ToLower(idx.Columns[0].Expression), "lower(") {
  327. t.Fatalf("unexpected expression text %q", idx.Columns[0].Expression)
  328. }
  329. }
  330. func TestJoinUsingMultipleColumns(t *testing.T) {
  331. _, schema, table := newTestDB(t)
  332. e := newExec(schema, table)
  333. execMust(t, e, "CREATE TABLE counts (site_id INTEGER, path_id INTEGER, total INTEGER)")
  334. execMust(t, e, "CREATE TABLE paths (site_id INTEGER, path_id INTEGER, path TEXT)")
  335. execMust(t, e, "INSERT INTO counts VALUES (1, 1, 4), (1, 2, 8), (2, 1, 16)")
  336. execMust(t, e, "INSERT INTO paths VALUES (1, 1, '/one'), (1, 2, '/two'), (2, 2, '/other')")
  337. res := execMust(t, e, "SELECT paths.path, counts.total FROM counts JOIN paths USING (site_id, path_id) ORDER BY counts.total")
  338. if res.RowCount != 2 || res.Rows[0][0] != "/one" || res.Rows[1][0] != "/two" {
  339. t.Fatalf("JOIN USING rows = %v", res.Rows)
  340. }
  341. }
  342. func TestJoinUsingInCommaFromList(t *testing.T) {
  343. _, schema, table := newTestDB(t)
  344. e := newExec(schema, table)
  345. execMust(t, e, "CREATE TABLE base (id INTEGER)")
  346. execMust(t, e, "CREATE TABLE left_rows (id INTEGER)")
  347. execMust(t, e, "CREATE TABLE right_rows (id INTEGER)")
  348. execMust(t, e, "INSERT INTO base VALUES (1)")
  349. execMust(t, e, "INSERT INTO left_rows VALUES (1)")
  350. execMust(t, e, "INSERT INTO right_rows VALUES (1), (2)")
  351. res := execMust(t, e, "SELECT left_rows.id FROM base, left_rows JOIN right_rows USING (id)")
  352. if res.RowCount != 1 {
  353. t.Fatalf("mixed JOIN USING returned %d rows, want 1: %v", res.RowCount, res.Rows)
  354. }
  355. }
  356. func TestSQLiteDynamicTypingAssignments(t *testing.T) {
  357. _, schema, table := newTestDB(t)
  358. e := newExec(schema, table)
  359. execMust(t, e, "CREATE TABLE settings (value VARCHAR)")
  360. execMust(t, e, "INSERT INTO settings VALUES (2)")
  361. execMust(t, e, "UPDATE settings SET value = X'0102'")
  362. res := execMust(t, e, "SELECT typeof(value), hex(value) FROM settings")
  363. if res.Rows[0][0] != "blob" || res.Rows[0][1] != "0102" {
  364. t.Fatalf("dynamic value = %v", res.Rows[0])
  365. }
  366. }
  367. func TestRenameTableAboveSingleBatchLimit(t *testing.T) {
  368. _, schema, table := newTestDB(t)
  369. e := newExec(schema, table)
  370. execMust(t, e, "CREATE TABLE old_rows (id INTEGER PRIMARY KEY, value TEXT)")
  371. // A rename writes two KV operations per row; 32,768 rows exceed PizzaKV's
  372. // 65,535-operation batch limit.
  373. rows := make([]storage.Row, 32768)
  374. for i := range rows {
  375. rows[i] = storage.Row{"id": int64(i + 1), "value": fmt.Sprintf("v%d", i+1)}
  376. }
  377. if _, err := table.InsertBulk("old_rows", rows); err != nil {
  378. t.Fatalf("insert rows: %v", err)
  379. }
  380. execMust(t, e, "ALTER TABLE old_rows RENAME TO new_rows")
  381. res := execMust(t, e, "SELECT count(*) FROM new_rows")
  382. if res.Rows[0][0] != int64(len(rows)) {
  383. t.Fatalf("renamed table has %v rows, want %d", res.Rows[0][0], len(rows))
  384. }
  385. }