github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/upsert_test.go (about) 1 // Copyright 2017 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package sql_test 12 13 import ( 14 "bytes" 15 "context" 16 "sync/atomic" 17 "testing" 18 19 "github.com/cockroachdb/cockroach/pkg/base" 20 "github.com/cockroachdb/cockroach/pkg/keys" 21 "github.com/cockroachdb/cockroach/pkg/kv/kvserver" 22 "github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvserverbase" 23 "github.com/cockroachdb/cockroach/pkg/roachpb" 24 "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" 25 "github.com/cockroachdb/cockroach/pkg/testutils/sqlutils" 26 "github.com/cockroachdb/cockroach/pkg/util/leaktest" 27 "golang.org/x/sync/errgroup" 28 ) 29 30 func TestUpsertFastPath(t *testing.T) { 31 defer leaktest.AfterTest(t)() 32 33 // This filter increments scans and endTxn for every ScanRequest and 34 // EndTxnRequest that hits user table data. 35 var scans uint64 36 var endTxn uint64 37 filter := func(filterArgs kvserverbase.FilterArgs) *roachpb.Error { 38 if bytes.Compare(filterArgs.Req.Header().Key, keys.UserTableDataMin) >= 0 { 39 switch filterArgs.Req.Method() { 40 case roachpb.Scan: 41 atomic.AddUint64(&scans, 1) 42 case roachpb.EndTxn: 43 if filterArgs.Hdr.Txn.Status == roachpb.STAGING { 44 // Ignore async explicit commits. 45 return nil 46 } 47 atomic.AddUint64(&endTxn, 1) 48 } 49 } 50 return nil 51 } 52 53 s, conn, _ := serverutils.StartServer(t, base.TestServerArgs{ 54 Knobs: base.TestingKnobs{Store: &kvserver.StoreTestingKnobs{ 55 EvalKnobs: kvserverbase.BatchEvalTestingKnobs{ 56 TestingEvalFilter: filter, 57 }, 58 }}, 59 }) 60 defer s.Stopper().Stop(context.Background()) 61 sqlDB := sqlutils.MakeSQLRunner(conn) 62 sqlDB.Exec(t, `CREATE DATABASE d`) 63 sqlDB.Exec(t, `CREATE TABLE d.kv (k INT PRIMARY KEY, v INT)`) 64 65 // This should hit the fast path. 66 atomic.StoreUint64(&scans, 0) 67 atomic.StoreUint64(&endTxn, 0) 68 sqlDB.Exec(t, `UPSERT INTO d.kv VALUES (1, 1)`) 69 if s := atomic.LoadUint64(&scans); s != 0 { 70 t.Errorf("expected no scans (the upsert fast path) but got %d", s) 71 } 72 if s := atomic.LoadUint64(&endTxn); s != 0 { 73 t.Errorf("expected no end-txn (1PC) but got %d", s) 74 } 75 76 // This could hit the fast path, but doesn't right now because of #14482. 77 atomic.StoreUint64(&scans, 0) 78 atomic.StoreUint64(&endTxn, 0) 79 sqlDB.Exec(t, `INSERT INTO d.kv VALUES (1, 1) ON CONFLICT (k) DO UPDATE SET v=excluded.v`) 80 if s := atomic.LoadUint64(&scans); s != 1 { 81 t.Errorf("expected 1 scans (no upsert fast path) but got %d", s) 82 } 83 if s := atomic.LoadUint64(&endTxn); s != 0 { 84 t.Errorf("expected no end-txn (1PC) but got %d", s) 85 } 86 87 // This should not hit the fast path because it doesn't set every column. 88 atomic.StoreUint64(&scans, 0) 89 atomic.StoreUint64(&endTxn, 0) 90 sqlDB.Exec(t, `UPSERT INTO d.kv (k) VALUES (1)`) 91 if s := atomic.LoadUint64(&scans); s != 1 { 92 t.Errorf("expected 1 scans (no upsert fast path) but got %d", s) 93 } 94 if s := atomic.LoadUint64(&endTxn); s != 0 { 95 t.Errorf("expected no end-txn (1PC) but got %d", s) 96 } 97 98 // This should hit the fast path, but won't be a 1PC because of the explicit 99 // transaction. 100 atomic.StoreUint64(&scans, 0) 101 atomic.StoreUint64(&endTxn, 0) 102 tx, err := conn.Begin() 103 if err != nil { 104 t.Fatal(err) 105 } 106 if _, err := tx.Exec(`UPSERT INTO d.kv VALUES (1, 1)`); err != nil { 107 t.Fatal(err) 108 } 109 if err := tx.Commit(); err != nil { 110 t.Fatal(err) 111 } 112 if s := atomic.LoadUint64(&scans); s != 0 { 113 t.Errorf("expected no scans (the upsert fast path) but got %d", s) 114 } 115 if s := atomic.LoadUint64(&endTxn); s != 1 { 116 t.Errorf("expected 1 end-txn (no 1PC) but got %d", s) 117 } 118 119 // This should not hit the fast path because kv has a secondary index. 120 sqlDB.Exec(t, `CREATE INDEX vidx ON d.kv (v)`) 121 atomic.StoreUint64(&scans, 0) 122 atomic.StoreUint64(&endTxn, 0) 123 sqlDB.Exec(t, `UPSERT INTO d.kv VALUES (1, 1)`) 124 if s := atomic.LoadUint64(&scans); s != 1 { 125 t.Errorf("expected 1 scans (no upsert fast path) but got %d", s) 126 } 127 if s := atomic.LoadUint64(&endTxn); s != 0 { 128 t.Errorf("expected no end-txn (1PC) but got %d", s) 129 } 130 } 131 132 func TestConcurrentUpsert(t *testing.T) { 133 defer leaktest.AfterTest(t)() 134 135 s, conn, _ := serverutils.StartServer(t, base.TestServerArgs{}) 136 defer s.Stopper().Stop(context.Background()) 137 sqlDB := sqlutils.MakeSQLRunner(conn) 138 139 sqlDB.Exec(t, `CREATE DATABASE d`) 140 sqlDB.Exec(t, `CREATE TABLE d.t (a INT PRIMARY KEY, b INT, INDEX b_idx (b))`) 141 142 testCases := []struct { 143 name string 144 updateStmt string 145 }{ 146 // Upsert case. 147 { 148 name: "upsert", 149 updateStmt: `UPSERT INTO d.t VALUES (1, $1)`, 150 }, 151 // Update case. 152 { 153 name: "update", 154 updateStmt: `UPDATE d.t SET b = $1 WHERE a = 1`, 155 }, 156 } 157 158 for _, test := range testCases { 159 t.Run(test.name, func(t *testing.T) { 160 g, ctx := errgroup.WithContext(context.Background()) 161 for i := 0; i < 2; i++ { 162 g.Go(func() error { 163 for j := 0; j < 100; j++ { 164 if _, err := sqlDB.DB.ExecContext(ctx, test.updateStmt, j); err != nil { 165 return err 166 } 167 } 168 return nil 169 }) 170 } 171 // We select on both the primary key and the secondary 172 // index to highlight the lost update anomaly, which used 173 // to occur on 1PC snapshot-isolation upserts (and updates). 174 // See #14099. 175 if err := g.Wait(); err != nil { 176 t.Errorf(`%+v 177 SELECT * FROM d.t@primary = %s 178 SELECT * FROM d.t@b_idx = %s 179 `, 180 err, 181 sqlDB.QueryStr(t, `SELECT * FROM d.t@primary`), 182 sqlDB.QueryStr(t, `SELECT * FROM d.t@b_idx`), 183 ) 184 } 185 }) 186 } 187 }