2
0

transaction_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package executor
  2. import (
  3. "fmt"
  4. "sync"
  5. "testing"
  6. "time"
  7. "github.com/danfragoso/pizzasql-next/pkg/storage"
  8. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  9. )
  10. func newTestDB(t *testing.T) (*storage.KVPool, *storage.SchemaManager, *storage.TableManager) {
  11. t.Helper()
  12. kv := testkv.New(t)
  13. pool := kv.Pool(8)
  14. t.Cleanup(func() { pool.Close() })
  15. schema := storage.NewSchemaManager(pool, "testdb")
  16. table := storage.NewTableManager(pool, schema, "testdb")
  17. return pool, schema, table
  18. }
  19. func newExec(schema *storage.SchemaManager, table *storage.TableManager) *Executor {
  20. e := New(schema, table)
  21. e.SyncCatalog()
  22. return e
  23. }
  24. func execMust(t *testing.T, e *Executor, sql string) *Result {
  25. t.Helper()
  26. res, err := execSQL(e, sql)
  27. if err != nil {
  28. t.Fatalf("exec %q: %v", sql, err)
  29. }
  30. return res
  31. }
  32. func TestSQLTransactionReadYourWrites(t *testing.T) {
  33. _, schema, table := newTestDB(t)
  34. e := newExec(schema, table)
  35. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  36. execMust(t, e, "BEGIN")
  37. execMust(t, e, "INSERT INTO t VALUES (1, 'one')")
  38. if res := execMust(t, e, "SELECT * FROM t WHERE id = 1"); res.RowCount != 1 {
  39. t.Fatalf("expected 1 row before commit, got %d", res.RowCount)
  40. }
  41. execMust(t, e, "COMMIT")
  42. if res := execMust(t, e, "SELECT * FROM t WHERE id = 1"); res.RowCount != 1 {
  43. t.Fatalf("expected 1 row after commit, got %d", res.RowCount)
  44. }
  45. }
  46. func TestSQLAutocommitDeleteWithSubquery(t *testing.T) {
  47. _, schema, table := newTestDB(t)
  48. e := newExec(schema, table)
  49. execMust(t, e, "CREATE TABLE organizations (id INTEGER PRIMARY KEY, active INTEGER)")
  50. execMust(t, e, "CREATE TABLE metrics (id INTEGER PRIMARY KEY, organization_id INTEGER)")
  51. execMust(t, e, "INSERT INTO organizations VALUES (1, 0)")
  52. execMust(t, e, "INSERT INTO organizations VALUES (2, 1)")
  53. execMust(t, e, "INSERT INTO metrics VALUES (10, 1)")
  54. execMust(t, e, "INSERT INTO metrics VALUES (20, 2)")
  55. type outcome struct {
  56. result *Result
  57. err error
  58. }
  59. done := make(chan outcome, 1)
  60. go func() {
  61. result, err := execSQL(e, "DELETE FROM metrics WHERE organization_id IN (SELECT id FROM organizations WHERE active = 0)")
  62. done <- outcome{result: result, err: err}
  63. }()
  64. select {
  65. case got := <-done:
  66. if got.err != nil {
  67. t.Fatal(got.err)
  68. }
  69. if got.result.RowsAffected != 1 {
  70. t.Fatalf("deleted %d rows, want 1", got.result.RowsAffected)
  71. }
  72. case <-time.After(time.Second):
  73. t.Fatal("DELETE with subquery deadlocked")
  74. }
  75. if result := execMust(t, e, "SELECT id FROM metrics"); result.RowCount != 1 || result.Rows[0][0] != int64(20) {
  76. t.Fatalf("remaining rows = %v", result.Rows)
  77. }
  78. }
  79. func TestSQLTransactionRollbackZeroDurableWrites(t *testing.T) {
  80. _, schema, table := newTestDB(t)
  81. e := newExec(schema, table)
  82. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  83. execMust(t, e, "BEGIN")
  84. execMust(t, e, "INSERT INTO t VALUES (1)")
  85. execMust(t, e, "INSERT INTO t VALUES (2)")
  86. execMust(t, e, "ROLLBACK")
  87. res := execMust(t, e, "SELECT COUNT(*) FROM t")
  88. if len(res.Rows) != 1 || res.Rows[0][0] != int64(0) {
  89. t.Fatalf("expected 0 rows after rollback, got %v", res.Rows)
  90. }
  91. }
  92. func TestSQLTransactionSavepoints(t *testing.T) {
  93. _, schema, table := newTestDB(t)
  94. e := newExec(schema, table)
  95. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  96. execMust(t, e, "BEGIN")
  97. execMust(t, e, "INSERT INTO t VALUES (1)")
  98. execMust(t, e, "SAVEPOINT sp1")
  99. execMust(t, e, "INSERT INTO t VALUES (2)")
  100. execMust(t, e, "ROLLBACK TO sp1")
  101. execMust(t, e, "COMMIT")
  102. if res := execMust(t, e, "SELECT COUNT(*) FROM t"); res.Rows[0][0] != int64(1) {
  103. t.Fatalf("expected 1 row after savepoint rollback, got %v", res.Rows[0][0])
  104. }
  105. if res := execMust(t, e, "SELECT * FROM t WHERE id = 2"); res.RowCount != 0 {
  106. t.Fatalf("row 2 should be discarded")
  107. }
  108. }
  109. func TestSQLTransactionConcurrentUpdate(t *testing.T) {
  110. _, schema, table := newTestDB(t)
  111. e1 := newExec(schema, table)
  112. e2 := newExec(schema, table)
  113. execMust(t, e1, "CREATE TABLE acct (id INTEGER PRIMARY KEY, bal INTEGER)")
  114. execMust(t, e1, "INSERT INTO acct VALUES (1, 100)")
  115. execMust(t, e1, "BEGIN")
  116. execMust(t, e2, "BEGIN")
  117. execMust(t, e1, "UPDATE acct SET bal = bal + 10 WHERE id = 1")
  118. execMust(t, e2, "UPDATE acct SET bal = bal + 20 WHERE id = 1")
  119. results := make(chan error, 2)
  120. go func() { _, err := execSQL(e1, "COMMIT"); results <- err }()
  121. go func() { _, err := execSQL(e2, "COMMIT"); results <- err }()
  122. errs := [2]error{<-results, <-results}
  123. ok, conflict := 0, 0
  124. for _, err := range errs {
  125. switch {
  126. case err == nil:
  127. ok++
  128. case err == storage.ErrSerialization:
  129. conflict++
  130. default:
  131. t.Fatalf("unexpected commit error: %v", err)
  132. }
  133. }
  134. if ok != 1 || conflict != 1 {
  135. t.Fatalf("expected exactly one commit and one conflict, got ok=%d conflict=%d", ok, conflict)
  136. }
  137. if res := execMust(t, e1, "SELECT bal FROM acct WHERE id = 1"); res.Rows[0][0] != int64(110) && res.Rows[0][0] != int64(120) {
  138. t.Fatalf("balance should be 110 or 120 (the winning update), got %v", res.Rows[0][0])
  139. }
  140. }
  141. func TestSQLAtomicMultiTableCommit(t *testing.T) {
  142. _, schema, table := newTestDB(t)
  143. e := newExec(schema, table)
  144. execMust(t, e, "CREATE TABLE a (id INTEGER PRIMARY KEY)")
  145. execMust(t, e, "CREATE TABLE b (id INTEGER PRIMARY KEY)")
  146. execMust(t, e, "BEGIN")
  147. execMust(t, e, "INSERT INTO a VALUES (1)")
  148. execMust(t, e, "INSERT INTO b VALUES (1)")
  149. execMust(t, e, "COMMIT")
  150. if res := execMust(t, e, "SELECT COUNT(*) FROM a"); res.Rows[0][0] != int64(1) {
  151. t.Fatalf("a count = %v", res.Rows[0][0])
  152. }
  153. if res := execMust(t, e, "SELECT COUNT(*) FROM b"); res.Rows[0][0] != int64(1) {
  154. t.Fatalf("b count = %v", res.Rows[0][0])
  155. }
  156. }
  157. func TestSQLTransactionJoinIgnoresUnrelatedRows(t *testing.T) {
  158. _, schema, table := newTestDB(t)
  159. e1 := newExec(schema, table)
  160. e2 := newExec(schema, table)
  161. execMust(t, e1, "CREATE TABLE products (id TEXT PRIMARY KEY, inventory INTEGER)")
  162. execMust(t, e1, "CREATE TABLE cart_items (id TEXT PRIMARY KEY, cart_id TEXT, product_id TEXT)")
  163. execMust(t, e1, "CREATE INDEX idx_cart_items_cart ON cart_items (cart_id)")
  164. execMust(t, e1, "CREATE TABLE orders (id TEXT PRIMARY KEY)")
  165. execMust(t, e1, "INSERT INTO products VALUES ('p1', 10)")
  166. execMust(t, e1, "INSERT INTO products VALUES ('p2', 20)")
  167. execMust(t, e1, "INSERT INTO cart_items VALUES ('i1', 'cart-a', 'p1')")
  168. execMust(t, e1, "BEGIN")
  169. if result := execMust(t, e1, "SELECT ci.id, p.inventory FROM cart_items ci JOIN products p ON ci.product_id = p.id WHERE ci.cart_id = 'cart-a'"); result.RowCount != 1 {
  170. t.Fatalf("join returned %d rows", result.RowCount)
  171. }
  172. execMust(t, e2, "UPDATE products SET inventory = 21 WHERE id = 'p2'")
  173. execMust(t, e2, "INSERT INTO cart_items VALUES ('i2', 'cart-b', 'p2')")
  174. execMust(t, e1, "INSERT INTO orders VALUES ('o1')")
  175. if _, err := execSQL(e1, "COMMIT"); err != nil {
  176. t.Fatalf("unrelated product/cart item caused conflict: %v", err)
  177. }
  178. }
  179. func TestSQLDuplicateInsert(t *testing.T) {
  180. _, schema, table := newTestDB(t)
  181. e := newExec(schema, table)
  182. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
  183. execMust(t, e, "INSERT INTO t VALUES (1)")
  184. if _, err := execSQL(e, "INSERT INTO t VALUES (1)"); err == nil {
  185. t.Fatal("expected duplicate insert to fail")
  186. }
  187. }
  188. func TestSQLPerTableConcurrentRowIDs(t *testing.T) {
  189. _, schema, table := newTestDB(t)
  190. e := newExec(schema, table)
  191. execMust(t, e, "CREATE TABLE t (name TEXT)")
  192. const n = 50
  193. var wg sync.WaitGroup
  194. errCh := make(chan error, n)
  195. for i := 0; i < n; i++ {
  196. wg.Add(1)
  197. go func(i int) {
  198. defer wg.Done()
  199. exec := newExec(schema, table)
  200. if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO t VALUES ('n%d')", i)); err != nil {
  201. errCh <- fmt.Errorf("insert %d: %v", i, err)
  202. }
  203. }(i)
  204. }
  205. wg.Wait()
  206. close(errCh)
  207. for err := range errCh {
  208. t.Fatal(err)
  209. }
  210. check := newExec(schema, table)
  211. res, err := execSQL(check, "SELECT COUNT(*) FROM t")
  212. if err != nil {
  213. t.Fatal(err)
  214. }
  215. if res.Rows[0][0] != int64(n) {
  216. t.Fatalf("expected %d rows, got %v", n, res.Rows[0][0])
  217. }
  218. }