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  }