rowid_cursor_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package executor
  2. import (
  3. "strconv"
  4. "sync"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/storage"
  7. )
  8. func rowIDs(t *testing.T, e *Executor, sql string) []int64 {
  9. t.Helper()
  10. res := execMust(t, e, sql)
  11. ids := make([]int64, 0, res.RowCount)
  12. for _, row := range res.Rows {
  13. id, ok := row[0].(int64)
  14. if !ok {
  15. t.Fatalf("rowid %v has type %T, want int64", row[0], row[0])
  16. }
  17. ids = append(ids, id)
  18. }
  19. return ids
  20. }
  21. func TestRowIDSelectableStableAcrossUpdateAndTypedBigint(t *testing.T) {
  22. _, schema, table := newTestDB(t)
  23. e := newExec(schema, table)
  24. execMust(t, e, "CREATE TABLE items (id TEXT PRIMARY KEY, name TEXT)")
  25. execMust(t, e, "INSERT INTO items (id, name) VALUES ('a', 'a'), ('b', 'b'), ('c', 'c')")
  26. res := execMust(t, e, "SELECT _rowid_, rowid, oid FROM items ORDER BY _rowid_")
  27. if res.RowCount != 3 {
  28. t.Fatalf("row count = %d, want 3", res.RowCount)
  29. }
  30. if got := res.GetColumnType(0); got != "BIGINT" {
  31. t.Fatalf("_rowid_ type = %q, want BIGINT", got)
  32. }
  33. if got := res.Rows[0][0]; got != int64(1) {
  34. t.Fatalf("first _rowid_ = %v, want 1", got)
  35. }
  36. for i := 1; i < res.RowCount; i++ {
  37. if res.Rows[i][0].(int64) <= res.Rows[i-1][0].(int64) {
  38. t.Fatalf("_rowid_ is not monotonically increasing: %v", res.Rows)
  39. }
  40. if res.Rows[i][0] != res.Rows[i][1] || res.Rows[i][0] != res.Rows[i][2] {
  41. t.Fatalf("rowid/oid aliases disagree: %v", res.Rows[i])
  42. }
  43. }
  44. execMust(t, e, "UPDATE items SET name = 'updated' WHERE id = 'b'")
  45. execMust(t, e, "UPDATE items SET id = 'bb' WHERE id = 'b'")
  46. ids := rowIDs(t, e, "SELECT _rowid_ FROM items WHERE id = 'bb'")
  47. if len(ids) != 1 || ids[0] != 2 {
  48. t.Fatalf("rowid after UPDATE = %v, want [2]", ids)
  49. }
  50. }
  51. func TestRowIDExplicitColumnTakesPrecedence(t *testing.T) {
  52. _, schema, table := newTestDB(t)
  53. e := newExec(schema, table)
  54. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, rowid TEXT)")
  55. execMust(t, e, "INSERT INTO t (id, rowid) VALUES (1, 'explicit')")
  56. res := execMust(t, e, "SELECT rowid, _rowid_ FROM t")
  57. if got := res.Rows[0][0]; got != "explicit" {
  58. t.Fatalf("explicit rowid column = %v, want explicit", got)
  59. }
  60. if got := res.Rows[0][1]; got != int64(1) {
  61. t.Fatalf("hidden _rowid_ = %v, want 1", got)
  62. }
  63. }
  64. func TestRowIDAllocationSurvivesManagerRestart(t *testing.T) {
  65. pool, schema, table := newTestDB(t)
  66. e := newExec(schema, table)
  67. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  68. execMust(t, e, "INSERT INTO t (id, v) VALUES (1, 'a'), (2, 'b'), (3, 'c')")
  69. // A new manager pair reads the durable schema and rowid allocator state.
  70. reopenedSchema := storage.NewSchemaManager(pool, "testdb")
  71. reopenedTable := storage.NewTableManager(pool, reopenedSchema, "testdb")
  72. reopened := newExec(reopenedSchema, reopenedTable)
  73. execMust(t, reopened, "INSERT INTO t (id, v) VALUES (4, 'd')")
  74. ids := rowIDs(t, reopened, "SELECT _rowid_ FROM t ORDER BY _rowid_")
  75. want := []int64{1, 2, 3, 4}
  76. if len(ids) != len(want) {
  77. t.Fatalf("rowids = %v, want %v", ids, want)
  78. }
  79. for i := range want {
  80. if ids[i] != want[i] {
  81. t.Fatalf("rowids = %v, want %v", ids, want)
  82. }
  83. }
  84. }
  85. func TestRowIDKeysetPaginationDoesNotSkipOrRepeat(t *testing.T) {
  86. _, schema, table := newTestDB(t)
  87. e := newExec(schema, table)
  88. execMust(t, e, "CREATE TABLE events (id INTEGER PRIMARY KEY, payload TEXT)")
  89. for i := 1; i <= 5; i++ {
  90. execMust(t, e, "INSERT INTO events (id, payload) VALUES ("+strconv.Itoa(i)+", 'x')")
  91. }
  92. // Paginate with literals so the test exercises the executor filter and order
  93. // path directly; insert new rows between pages to prove the cursor stays
  94. // monotonic.
  95. var seen []int64
  96. cursor := int64(0)
  97. for {
  98. res := execMust(t, e, "SELECT _rowid_ FROM events WHERE _rowid_ > "+strconv.FormatInt(cursor, 10)+" ORDER BY _rowid_ LIMIT 3")
  99. if res.RowCount == 0 {
  100. break
  101. }
  102. for _, row := range res.Rows {
  103. id := row[0].(int64)
  104. if len(seen) > 0 && id <= seen[len(seen)-1] {
  105. t.Fatalf("rowid repeated or out of order: seen=%v next=%d", seen, id)
  106. }
  107. seen = append(seen, id)
  108. }
  109. cursor = seen[len(seen)-1]
  110. if cursor == 3 {
  111. // New rows arriving after the first page must appear later.
  112. execMust(t, e, "INSERT INTO events (id, payload) VALUES (100, 'late')")
  113. }
  114. }
  115. if len(seen) != 6 {
  116. t.Fatalf("paginated %v, want 6 rows including the late insert", seen)
  117. }
  118. }
  119. func TestRowIDUniqueAndMonotonicUnderConcurrentInsert(t *testing.T) {
  120. _, schema, table := newTestDB(t)
  121. e := newExec(schema, table)
  122. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")
  123. const workers, perWorker = 4, 20
  124. var wg sync.WaitGroup
  125. var mu sync.Mutex
  126. var ids []int64
  127. for w := 0; w < workers; w++ {
  128. wg.Add(1)
  129. go func(worker int) {
  130. defer wg.Done()
  131. for i := 0; i < perWorker; i++ {
  132. id := strconv.Itoa(worker*perWorker + i + 1)
  133. if _, err := execSQL(e, "INSERT INTO t (id, v) VALUES ("+id+", 'x')"); err != nil {
  134. t.Errorf("insert %s: %v", id, err)
  135. return
  136. }
  137. res, err := execSQL(e, "SELECT _rowid_ FROM t WHERE id = "+id)
  138. if err != nil {
  139. t.Errorf("select %s: %v", id, err)
  140. return
  141. }
  142. mu.Lock()
  143. ids = append(ids, res.Rows[0][0].(int64))
  144. mu.Unlock()
  145. }
  146. }(w)
  147. }
  148. wg.Wait()
  149. seen := make(map[int64]bool, len(ids))
  150. for _, id := range ids {
  151. if seen[id] {
  152. t.Fatalf("duplicate rowid %d", id)
  153. }
  154. seen[id] = true
  155. }
  156. if len(seen) != workers*perWorker {
  157. t.Fatalf("got %d unique rowids, want %d", len(seen), workers*perWorker)
  158. }
  159. }
  160. // INTEGER PRIMARY KEY is SQLite's rowid alias: a row inserted with id=10 gets
  161. // _rowid_=10. UPDATE never rewrites the stored _rowid_, so a keyset cursor is
  162. // stable across value and primary-key updates even though the rowid is not
  163. // independent of the key. Clients must not insert descending integer keys if
  164. // they rely on rowid monotonicity.
  165. func TestRowIDIntegerPrimaryKeyAliasIsStableAcrossUpdate(t *testing.T) {
  166. _, schema, table := newTestDB(t)
  167. e := newExec(schema, table)
  168. execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
  169. execMust(t, e, "INSERT INTO t (id, name) VALUES (10, 'a'), (20, 'b'), (30, 'c')")
  170. res := execMust(t, e, "SELECT id, _rowid_ FROM t ORDER BY id")
  171. for _, row := range res.Rows {
  172. if row[0] != row[1] {
  173. t.Fatalf("integer primary key %v does not alias _rowid_ %v", row[0], row[1])
  174. }
  175. }
  176. execMust(t, e, "UPDATE t SET id = 200, name = 'updated' WHERE id = 20")
  177. res = execMust(t, e, "SELECT id, _rowid_ FROM t WHERE id = 200")
  178. if got := res.Rows[0][1]; got != int64(20) {
  179. t.Fatalf("rowid after primary-key update = %v, want the original 20", got)
  180. }
  181. }