| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- package storage
- import (
- "fmt"
- "sync"
- "testing"
- "time"
- )
- func newTestSession(t *testing.T) (*testKVServer, *KVPool, *SchemaManager, *TableManager) {
- t.Helper()
- kv := newTestKVServer(t)
- pool := newTestKVPool(kv, 8, 5*time.Second)
- schemas := NewSchemaManager(pool, "testdb")
- tables := NewTableManager(pool, schemas, "testdb")
- t.Cleanup(func() { pool.Close() })
- return kv, pool, schemas, tables
- }
- func createTestTable(t *testing.T, schemas *SchemaManager, name string, cols []Column) {
- t.Helper()
- if err := schemas.CreateTable(&Schema{Name: name, Columns: cols}); err != nil {
- t.Fatalf("create table %s: %v", name, err)
- }
- }
- func TestSessionReadYourWrites(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}, {Name: "v", Type: "TEXT"}})
- s := NewSession(schemas, tables)
- if err := s.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s.Insert("t", Row{"id": int64(1), "v": "one"}); err != nil {
- t.Fatal(err)
- }
- // Point read observes the staged overlay before commit.
- row, err := s.GetByPK("t", "1")
- if err != nil || row["v"] != "one" {
- t.Fatalf("read-your-writes GetByPK: row=%v err=%v", row, err)
- }
- // Scan observes the staged overlay before commit.
- rows, err := s.Select("t", nil)
- if err != nil || len(rows) != 1 || rows[0]["v"] != "one" {
- t.Fatalf("read-your-writes Select: rows=%v err=%v", rows, err)
- }
- if err := s.Commit(); err != nil {
- t.Fatal(err)
- }
- // Still visible from a fresh read after commit.
- if row, err := tables.GetByPK("t", "1"); err != nil || row["v"] != "one" {
- t.Fatalf("post-commit GetByPK: row=%v err=%v", row, err)
- }
- }
- func TestSessionIndexedReadTracksOnlyMatches(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "items", []Column{
- {Name: "id", Type: "INTEGER", PrimaryKey: true},
- {Name: "cart_id", Type: "TEXT"},
- })
- if err := schemas.CreateIndex(&Index{
- Name: "idx_items_cart", Table: "items",
- Columns: []IndexColumn{{Name: "cart_id"}},
- }); err != nil {
- t.Fatal(err)
- }
- rows := make([]Row, 50)
- for i := range rows {
- rows[i] = Row{"id": int64(i + 1), "cart_id": fmt.Sprintf("cart-%d", i)}
- }
- if _, err := tables.InsertBulk("items", rows); err != nil {
- t.Fatal(err)
- }
- if err := tables.BuildIndex("idx_items_cart", "items", []string{"cart_id"}); err != nil {
- t.Fatal(err)
- }
- s := NewSession(schemas, tables)
- if err := s.Begin(); err != nil {
- t.Fatal(err)
- }
- got, err := s.SelectByIndex("items", "idx_items_cart", "cart-37")
- if err != nil {
- t.Fatal(err)
- }
- if len(got) != 1 || got[0]["id"] != int64(38) {
- t.Fatalf("indexed rows = %#v", got)
- }
- if len(s.reads) != 1 {
- t.Fatalf("indexed transaction captured %d row versions, want 1", len(s.reads))
- }
- if err := s.Rollback(); err != nil {
- t.Fatal(err)
- }
- }
- func TestSessionIndexedPredicateConflictsOnlyOnMatchingValue(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "items", []Column{
- {Name: "id", Type: "INTEGER", PrimaryKey: true},
- {Name: "cart_id", Type: "TEXT"},
- })
- if err := schemas.CreateIndex(&Index{
- Name: "idx_items_cart", Table: "items",
- Columns: []IndexColumn{{Name: "cart_id"}},
- }); err != nil {
- t.Fatal(err)
- }
- if err := tables.Insert("items", Row{"id": int64(1), "cart_id": "cart-a"}); err != nil {
- t.Fatal(err)
- }
- if err := tables.BuildIndex("idx_items_cart", "items", []string{"cart_id"}); err != nil {
- t.Fatal(err)
- }
- unrelated := NewSession(schemas, tables)
- if err := unrelated.Begin(); err != nil {
- t.Fatal(err)
- }
- if _, err := unrelated.SelectByIndex("items", "idx_items_cart", "cart-a"); err != nil {
- t.Fatal(err)
- }
- if err := tables.Insert("items", Row{"id": int64(2), "cart_id": "cart-b"}); err != nil {
- t.Fatal(err)
- }
- if err := unrelated.Commit(); err != nil {
- t.Fatalf("unrelated indexed insert caused conflict: %v", err)
- }
- matching := NewSession(schemas, tables)
- if err := matching.Begin(); err != nil {
- t.Fatal(err)
- }
- if _, err := matching.SelectByIndex("items", "idx_items_cart", "cart-c"); err != nil {
- t.Fatal(err)
- }
- if err := tables.Insert("items", Row{"id": int64(3), "cart_id": "cart-c"}); err != nil {
- t.Fatal(err)
- }
- if err := matching.Commit(); err != ErrSerialization {
- t.Fatalf("matching indexed insert commit error = %v, want ErrSerialization", err)
- }
- }
- func TestSessionRollbackZeroDurableWrites(t *testing.T) {
- kv, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}})
- s := NewSession(schemas, tables)
- if err := s.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s.Insert("t", Row{"id": int64(1)}); err != nil {
- t.Fatal(err)
- }
- if _, deleted, err := s.DeleteByPK("t", "1"); err != nil || !deleted {
- t.Fatalf("delete staged row: deleted=%v err=%v", deleted, err)
- }
- if err := s.Insert("t", Row{"id": int64(2)}); err != nil {
- t.Fatal(err)
- }
- if err := s.Rollback(); err != nil {
- t.Fatal(err)
- }
- if got := kv.countKeys("testdb:_data:t:"); got != 0 {
- t.Fatalf("rollback left %d durable rows", got)
- }
- }
- func TestSessionSavepoints(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}})
- s := NewSession(schemas, tables)
- if err := s.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s.Insert("t", Row{"id": int64(1)}); err != nil {
- t.Fatal(err)
- }
- sp := s.Snapshot()
- if err := s.Insert("t", Row{"id": int64(2)}); err != nil {
- t.Fatal(err)
- }
- s.RollbackTo(sp)
- if err := s.Commit(); err != nil {
- t.Fatal(err)
- }
- if _, err := tables.GetByPK("t", "1"); err != nil {
- t.Fatalf("row 1 should be committed: %v", err)
- }
- if _, err := tables.GetByPK("t", "2"); err != ErrKeyNotFound {
- t.Fatalf("row 2 should be discarded, err=%v", err)
- }
- }
- func TestSessionAtomicMultiTableCommit(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "a", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}})
- createTestTable(t, schemas, "b", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}})
- s1 := NewSession(schemas, tables)
- s2 := NewSession(schemas, tables)
- if err := s1.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s2.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s1.Insert("a", Row{"id": int64(1)}); err != nil {
- t.Fatal(err)
- }
- if err := s1.Insert("b", Row{"id": int64(1)}); err != nil {
- t.Fatal(err)
- }
- if err := s2.Insert("a", Row{"id": int64(1)}); err != nil {
- t.Fatal(err)
- }
- if err := s2.Insert("b", Row{"id": int64(2)}); err != nil {
- t.Fatal(err)
- }
- if err := s1.Commit(); err != nil {
- t.Fatal(err)
- }
- if err := s2.Commit(); err != ErrSerialization {
- t.Fatalf("expected s2 commit to conflict, got %v", err)
- }
- // s1 committed both rows; s2 committed neither (atomic rollback).
- if _, err := tables.GetByPK("a", "1"); err != nil {
- t.Fatalf("a/1 missing: %v", err)
- }
- if _, err := tables.GetByPK("b", "1"); err != nil {
- t.Fatalf("b/1 missing: %v", err)
- }
- if _, err := tables.GetByPK("b", "2"); err != ErrKeyNotFound {
- t.Fatalf("b/2 should be absent after s2 conflict, err=%v", err)
- }
- }
- func TestSessionConflictingUpdateExactlyOneCommits(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "acct", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}, {Name: "bal", Type: "INTEGER"}})
- if err := tables.Insert("acct", Row{"id": int64(1), "bal": int64(100)}); err != nil {
- t.Fatal(err)
- }
- s1 := NewSession(schemas, tables)
- s2 := NewSession(schemas, tables)
- update := func(s *Session) (func() error, error) {
- if err := s.Begin(); err != nil {
- return nil, err
- }
- row, err := s.GetByPK("acct", "1")
- if err != nil {
- return nil, err
- }
- if _, _, err := s.UpdateByPK("acct", "1", func(Row) (Row, error) {
- return Row{"bal": row["bal"].(int64) + 10}, nil
- }); err != nil {
- return nil, err
- }
- return s.Commit, nil
- }
- c1, err := update(s1)
- if err != nil {
- t.Fatal(err)
- }
- c2, err := update(s2)
- if err != nil {
- t.Fatal(err)
- }
- errs := make([]error, 2)
- var wg sync.WaitGroup
- wg.Add(2)
- go func() { defer wg.Done(); errs[0] = c1() }()
- go func() { defer wg.Done(); errs[1] = c2() }()
- wg.Wait()
- ok, conflict := 0, 0
- for _, e := range errs {
- if e == nil {
- ok++
- } else if e == ErrSerialization {
- conflict++
- } else {
- t.Fatalf("unexpected commit error: %v", e)
- }
- }
- if ok != 1 || conflict != 1 {
- t.Fatalf("expected exactly one commit and one conflict, got ok=%d conflict=%d", ok, conflict)
- }
- row, err := tables.GetByPK("acct", "1")
- if err != nil || row["bal"] != int64(110) {
- t.Fatalf("balance should be 110, got %v err=%v", row, err)
- }
- }
- func TestSessionNonConflictingConcurrentTransactions(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}, {Name: "v", Type: "TEXT"}})
- s1 := NewSession(schemas, tables)
- s2 := NewSession(schemas, tables)
- if err := s1.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s2.Begin(); err != nil {
- t.Fatal(err)
- }
- if err := s1.Insert("t", Row{"id": int64(1), "v": "a"}); err != nil {
- t.Fatal(err)
- }
- if err := s2.Insert("t", Row{"id": int64(2), "v": "b"}); err != nil {
- t.Fatal(err)
- }
- var wg sync.WaitGroup
- errs := make([]error, 2)
- wg.Add(2)
- go func() { defer wg.Done(); errs[0] = s1.Commit() }()
- go func() { defer wg.Done(); errs[1] = s2.Commit() }()
- wg.Wait()
- for i, e := range errs {
- if e != nil {
- t.Fatalf("commit %d failed: %v", i, e)
- }
- }
- if rows, err := tables.Select("t", nil); err != nil || len(rows) != 2 {
- t.Fatalf("expected 2 rows, got %d err=%v", len(rows), err)
- }
- }
- func TestDuplicateInsertConcurrent(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}})
- const n = 32
- errs := make([]error, n)
- var wg sync.WaitGroup
- for i := 0; i < n; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- errs[i] = tables.Insert("t", Row{"id": int64(1)})
- }()
- }
- wg.Wait()
- ok := 0
- for _, e := range errs {
- if e == nil {
- ok++
- } else if !(e != nil && fmt.Sprintf("%s", e) == "duplicate primary key: 1") {
- t.Fatalf("unexpected insert error: %v", e)
- }
- }
- if ok != 1 {
- t.Fatalf("expected exactly one successful insert, got %d", ok)
- }
- if got := kvCount(t, tables, "t"); got != 1 {
- t.Fatalf("expected 1 row, got %d", got)
- }
- }
- func kvCount(t *testing.T, tables *TableManager, table string) int {
- t.Helper()
- n, err := tables.CountFast(table)
- if err != nil {
- t.Fatal(err)
- }
- return n
- }
- func TestPerTableConcurrentRowIDs(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "name", Type: "TEXT"}}) // _rowid_ PK
- const n = 100
- var wg sync.WaitGroup
- errCh := make(chan error, n)
- for i := 0; i < n; i++ {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- if err := tables.Insert("t", Row{"name": fmt.Sprintf("n%d", i)}); err != nil {
- errCh <- fmt.Errorf("insert %d: %v", i, err)
- }
- }(i)
- }
- wg.Wait()
- close(errCh)
- for err := range errCh {
- t.Fatal(err)
- }
- if rows, err := tables.Select("t", nil); err != nil || len(rows) != n {
- t.Fatalf("expected %d rows, got %d err=%v", n, len(rows), err)
- }
- }
- func TestPointOpsDifferentKeysProgressConcurrently(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- createTestTable(t, schemas, "t", []Column{{Name: "id", Type: "TEXT", PrimaryKey: true}, {Name: "v", Type: "INTEGER"}})
- const n = 50
- var wg sync.WaitGroup
- errCh := make(chan error, n*3)
- for i := 0; i < n; i++ {
- wg.Add(1)
- go func(i int) {
- defer wg.Done()
- key := fmt.Sprintf("k%d", i)
- if err := tables.Insert("t", Row{"id": key, "v": int64(i)}); err != nil {
- errCh <- fmt.Errorf("insert %s: %v", key, err)
- return
- }
- if _, _, err := tables.UpdateByPK("t", key, func(Row) (Row, error) { return Row{"v": int64(i + 100)}, nil }); err != nil {
- errCh <- fmt.Errorf("update %s: %v", key, err)
- return
- }
- if _, err := tables.GetByPK("t", key); err != nil {
- errCh <- fmt.Errorf("get %s: %v", key, err)
- }
- }(i)
- }
- done := make(chan struct{})
- go func() { wg.Wait(); close(done) }()
- select {
- case <-done:
- case <-time.After(10 * time.Second):
- t.Fatal("point operations on distinct keys did not progress concurrently")
- }
- close(errCh)
- for err := range errCh {
- t.Fatal(err)
- }
- }
|