github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/routing/connector_test.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package routing
    22  
    23  import (
    24  	"context"
    25  	"testing"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  
    29  	"sort"
    30  
    31  	"reflect"
    32  
    33  	"github.com/pkg/errors"
    34  	"github.com/satori/go.uuid"
    35  	"github.com/uber-go/dosa"
    36  	"github.com/uber-go/dosa/connectors/devnull"
    37  	"github.com/uber-go/dosa/connectors/memory"
    38  	"github.com/uber-go/dosa/connectors/random"
    39  )
    40  
    41  const idcount = 10
    42  
    43  var (
    44  	cfg = Config{
    45  		Routers: Routers{
    46  			buildRouter("production", "map", "memory"),
    47  			buildRouter("production", "default", "random"),
    48  			buildRouter("ebook", "ebook-store", "memory"),
    49  			buildRouter("ebook", "default", "random"),
    50  			buildRouter("ebook", "apple.*", "memory"),
    51  			buildRouter("ebook", "*", "devnull"),
    52  			buildRouter("development", "map", "memory"),
    53  			buildRouter("development", "default", "random"),
    54  			buildRouter("default", "default", "memory"),
    55  		},
    56  	}
    57  	testInfo = &dosa.EntityInfo{
    58  		Ref: &dosa.SchemaRef{
    59  			Scope:      "production",
    60  			NamePrefix: "map",
    61  			EntityName: "testEntityName",
    62  		},
    63  		Def: &dosa.EntityDefinition{
    64  			Columns: []*dosa.ColumnDefinition{
    65  				{Name: "p1", Type: dosa.String},
    66  				{Name: "c1", Type: dosa.Int64},
    67  				{Name: "c2", Type: dosa.Double},
    68  				{Name: "c3", Type: dosa.String},
    69  				{Name: "c4", Type: dosa.Blob},
    70  				{Name: "c5", Type: dosa.Bool},
    71  				{Name: "c6", Type: dosa.Int32},
    72  				{Name: "c7", Type: dosa.TUUID},
    73  			},
    74  			Key: &dosa.PrimaryKey{
    75  				PartitionKeys: []string{"p1"},
    76  			},
    77  			Name: "t1",
    78  			Indexes: map[string]*dosa.IndexDefinition{
    79  				"i1": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
    80  		},
    81  	}
    82  	testInfoRandom = &dosa.EntityInfo{
    83  		Ref: &dosa.SchemaRef{
    84  			Scope:      "production",
    85  			NamePrefix: "default",
    86  			EntityName: "testEntityName",
    87  		},
    88  		Def: &dosa.EntityDefinition{
    89  			Columns: []*dosa.ColumnDefinition{
    90  				{Name: "p1", Type: dosa.String},
    91  				{Name: "c1", Type: dosa.Int64},
    92  				{Name: "c2", Type: dosa.Double},
    93  				{Name: "c3", Type: dosa.String},
    94  				{Name: "c4", Type: dosa.Blob},
    95  				{Name: "c5", Type: dosa.Bool},
    96  				{Name: "c6", Type: dosa.Int32},
    97  				{Name: "c7", Type: dosa.TUUID},
    98  			},
    99  			Key: &dosa.PrimaryKey{
   100  				PartitionKeys: []string{"p1"},
   101  			},
   102  			Name: "t1",
   103  			Indexes: map[string]*dosa.IndexDefinition{
   104  				"i1": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
   105  		},
   106  	}
   107  	clusteredEi = &dosa.EntityInfo{
   108  		Ref: &dosa.SchemaRef{
   109  			Scope:      "ebook",
   110  			NamePrefix: "apple.v1",
   111  			EntityName: "testEntityName",
   112  		},
   113  		Def: &dosa.EntityDefinition{
   114  			Columns: []*dosa.ColumnDefinition{
   115  				{Name: "f1", Type: dosa.String},
   116  				{Name: "c1", Type: dosa.Int64},
   117  				{Name: "c2", Type: dosa.Double},
   118  				{Name: "c3", Type: dosa.String},
   119  				{Name: "c4", Type: dosa.Blob},
   120  				{Name: "c5", Type: dosa.Bool},
   121  				{Name: "c6", Type: dosa.Int32},
   122  				{Name: "c7", Type: dosa.TUUID},
   123  			},
   124  			Key: &dosa.PrimaryKey{
   125  				PartitionKeys: []string{"f1"},
   126  				ClusteringKeys: []*dosa.ClusteringKey{
   127  					{Name: "c1", Descending: false},
   128  					{Name: "c7", Descending: true},
   129  				},
   130  			},
   131  			Name: "t2",
   132  			Indexes: map[string]*dosa.IndexDefinition{
   133  				"i2": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
   134  		},
   135  	}
   136  	testNoMatchInfo = &dosa.EntityInfo{
   137  		Ref: &dosa.SchemaRef{
   138  			Scope:      "weirdScope",
   139  			NamePrefix: "testPrefix",
   140  			EntityName: "testEntityName",
   141  		},
   142  		Def: &dosa.EntityDefinition{
   143  			Columns: []*dosa.ColumnDefinition{
   144  				{Name: "p1", Type: dosa.String},
   145  				{Name: "c1", Type: dosa.Int64},
   146  				{Name: "c2", Type: dosa.Double},
   147  				{Name: "c3", Type: dosa.String},
   148  				{Name: "c4", Type: dosa.Blob},
   149  				{Name: "c5", Type: dosa.Bool},
   150  				{Name: "c6", Type: dosa.Int32},
   151  				{Name: "c7", Type: dosa.TUUID},
   152  			},
   153  			Key: &dosa.PrimaryKey{
   154  				PartitionKeys: []string{"p1"},
   155  			},
   156  			Name: "t1",
   157  			Indexes: map[string]*dosa.IndexDefinition{
   158  				"i1": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
   159  		},
   160  	}
   161  	testPairs = dosa.FieldNameValuePair{}
   162  	ctx       = context.Background()
   163  )
   164  
   165  func getConnectorMap() map[string]dosa.Connector {
   166  	return map[string]dosa.Connector{
   167  		"memory":  memory.NewConnector(),
   168  		"devnull": devnull.NewConnector(),
   169  		"random":  random.NewConnector(),
   170  	}
   171  }
   172  
   173  func TestGetConnector(t *testing.T) {
   174  	connectorMap := getConnectorMap()
   175  	// no plugin
   176  	// glob match
   177  	rc := NewConnector(cfg, connectorMap, nil)
   178  	ei := &dosa.EntityInfo{
   179  		Ref: &dosa.SchemaRef{Scope: "ebook", NamePrefix: "apple.v1"},
   180  	}
   181  	conn, err := rc.getConnector(ei.Ref.Scope, ei.Ref.NamePrefix, "Read")
   182  	assert.Nil(t, err)
   183  	assert.NotNil(t, conn)
   184  
   185  	// exact match
   186  	ei.Ref.NamePrefix = "ebook-store"
   187  	conn, err = rc.getConnector(ei.Ref.Scope, ei.Ref.NamePrefix, "Read")
   188  	assert.Nil(t, err)
   189  	assert.NotNil(t, conn)
   190  
   191  	// match "*"
   192  	conn, err = rc.getConnector(ei.Ref.Scope, "*", "Read")
   193  	assert.Nil(t, err)
   194  	assert.NotNil(t, conn)
   195  
   196  	// match default
   197  	ei = &dosa.EntityInfo{
   198  		Ref: &dosa.SchemaRef{Scope: "notexist", NamePrefix: "apple.v1"},
   199  	}
   200  	conn, err = rc.getConnector(ei.Ref.Scope, ei.Ref.NamePrefix, "Read")
   201  	assert.Nil(t, err)
   202  	assert.NotNil(t, conn)
   203  	assert.Equal(t, reflect.TypeOf(conn), reflect.TypeOf(memory.NewConnector()))
   204  
   205  	// with plugin
   206  	rc.PluginFunc = func(scope, namePrefix, opName string) (string, string, error) {
   207  		return "ebook", "ebook-store", nil
   208  	}
   209  
   210  	conn, err = rc.getConnector(ei.Ref.Scope, ei.Ref.NamePrefix, "Read")
   211  	assert.NoError(t, err)
   212  	assert.NotNil(t, conn)
   213  
   214  	// plugin returns error
   215  	rc.PluginFunc = func(scope, namePrefix, opName string) (string, string, error) {
   216  		return "", "", errors.New("")
   217  	}
   218  	conn, err = rc.getConnector(ei.Ref.Scope, ei.Ref.NamePrefix, "Read")
   219  	assert.Contains(t, err.Error(), "failed to execute getConnector due to Plugin function error")
   220  	assert.Nil(t, conn)
   221  }
   222  
   223  func TestConnector_CreateIfNotExists(t *testing.T) {
   224  	connectorMap := getConnectorMap()
   225  	rc := NewConnector(cfg, connectorMap, nil)
   226  
   227  	assert.NoError(t, rc.CreateIfNotExists(ctx, testInfo, map[string]dosa.FieldValue{
   228  		"p1": dosa.FieldValue("data")}))
   229  
   230  	err := rc.CreateIfNotExists(ctx, testInfo, map[string]dosa.FieldValue{
   231  		"p1": dosa.FieldValue("data")})
   232  	assert.True(t, dosa.ErrorIsAlreadyExists(err))
   233  }
   234  
   235  func TestConnector_CreateIfNotExistsDefaultScope(t *testing.T) {
   236  	connectorMap := getConnectorMap()
   237  	rc := NewConnector(cfg, connectorMap, nil)
   238  
   239  	// not exist scope, use default
   240  	err := rc.CreateIfNotExists(ctx, testNoMatchInfo, map[string]dosa.FieldValue{
   241  		"p1": dosa.FieldValue("data")})
   242  	assert.NoError(t, err)
   243  
   244  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   245  		return "", "", errors.New("dummy errors")
   246  	}
   247  	rc.PluginFunc = plugin
   248  	// not exist scope, use default
   249  	err = rc.CreateIfNotExists(ctx, testNoMatchInfo, map[string]dosa.FieldValue{
   250  		"p1": dosa.FieldValue("data")})
   251  	assert.Contains(t, err.Error(), "dummy errors")
   252  }
   253  
   254  func TestConnector_CreateIfNotExists2(t *testing.T) {
   255  	connectorMap := getConnectorMap()
   256  	rc := NewConnector(cfg, connectorMap, nil)
   257  
   258  	testUUIDs := make([]dosa.UUID, 10)
   259  	for x := 0; x < 10; x++ {
   260  		testUUIDs[x] = dosa.NewUUID()
   261  	}
   262  
   263  	// first, insert 10 random UUID values into same partition key
   264  	for x := 0; x < 10; x++ {
   265  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   266  			"f1": dosa.FieldValue("data"),
   267  			"c1": dosa.FieldValue(int64(1)),
   268  			"c7": dosa.FieldValue(testUUIDs[x])})
   269  		assert.NoError(t, err)
   270  	}
   271  	// attempt to insert them all again
   272  	for x := 0; x < 10; x++ {
   273  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   274  			"f1": dosa.FieldValue("data"),
   275  			"c1": dosa.FieldValue(int64(1)),
   276  			"c7": dosa.FieldValue(testUUIDs[x])})
   277  		assert.Error(t, err, string(testUUIDs[x]))
   278  		assert.True(t, dosa.ErrorIsAlreadyExists(err))
   279  	}
   280  	// now, insert them again, but this time with a different secondary key
   281  	for x := 0; x < 10; x++ {
   282  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   283  			"f1": dosa.FieldValue("data"),
   284  			"c1": dosa.FieldValue(int64(2)),
   285  			"c7": dosa.FieldValue(testUUIDs[x])})
   286  		assert.NoError(t, err)
   287  	}
   288  	// and with a different primary key
   289  	for x := 0; x < 10; x++ {
   290  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   291  			"f1": dosa.FieldValue("different"),
   292  			"c1": dosa.FieldValue(int64(1)),
   293  			"c7": dosa.FieldValue(testUUIDs[x])})
   294  		assert.NoError(t, err)
   295  	}
   296  	data, token, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   297  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   298  	}, dosa.All(), "", 200)
   299  	assert.NoError(t, err)
   300  	assert.Empty(t, token)
   301  	assert.Len(t, data, 20)
   302  }
   303  
   304  func TestConnector_Read(t *testing.T) {
   305  	connectorMap := getConnectorMap()
   306  	rc := NewConnector(cfg, connectorMap, nil)
   307  
   308  	// read with no data
   309  	val, err := rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   310  		"p1": dosa.FieldValue("data"),
   311  	}, []string{"c1"})
   312  	assert.Nil(t, val)
   313  	assert.True(t, dosa.ErrorIsNotFound(err))
   314  
   315  	// read with no key field
   316  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{}, dosa.All())
   317  	assert.Nil(t, val)
   318  	assert.Contains(t, err.Error(), `partition key "p1"`)
   319  
   320  	// normal read
   321  	rc.CreateIfNotExists(ctx, testInfo, map[string]dosa.FieldValue{
   322  		"p1": dosa.FieldValue("data"),
   323  		"c1": dosa.FieldValue(int64(1)),
   324  		"c2": dosa.FieldValue(float64(2)),
   325  	})
   326  
   327  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   328  		"p1": dosa.FieldValue("data"),
   329  	}, []string{"c1"})
   330  	assert.NoError(t, err)
   331  	assert.Equal(t, int64(1), val["c1"])
   332  
   333  	// read all fields
   334  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   335  		"p1": dosa.FieldValue("data"),
   336  	}, dosa.All())
   337  	assert.NoError(t, err)
   338  	assert.Equal(t, int64(1), val["c1"])
   339  	assert.Equal(t, float64(2), val["c2"])
   340  	assert.Equal(t, "data", val["p1"])
   341  
   342  	// read a key that isn't there
   343  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   344  		"p1": dosa.FieldValue("not there"),
   345  	}, dosa.All())
   346  	assert.True(t, dosa.ErrorIsNotFound(err))
   347  
   348  	// now delete the one that is
   349  	err = rc.Remove(ctx, testInfo, map[string]dosa.FieldValue{
   350  		"p1": dosa.FieldValue("data")})
   351  	assert.NoError(t, err)
   352  
   353  	// read the deleted key
   354  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   355  		"p1": dosa.FieldValue("data")}, dosa.All())
   356  	assert.True(t, dosa.ErrorIsNotFound(err))
   357  
   358  	// insert into clustered entity
   359  	id := dosa.NewUUID()
   360  
   361  	err = rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   362  		"f1": dosa.FieldValue("key"),
   363  		"c1": dosa.FieldValue(int64(1)),
   364  		"c2": dosa.FieldValue(float64(1.2)),
   365  		"c7": dosa.FieldValue(id)})
   366  	assert.NoError(t, err)
   367  
   368  	// read that row
   369  	val, err = rc.Read(ctx, clusteredEi, map[string]dosa.FieldValue{
   370  		"f1": dosa.FieldValue("key"),
   371  		"c1": dosa.FieldValue(int64(1)),
   372  		"c7": dosa.FieldValue(id)}, dosa.All())
   373  	assert.NoError(t, err)
   374  	assert.Equal(t, dosa.FieldValue(float64(1.2)), val["c2"])
   375  
   376  	// and fail a read on a clustered key
   377  	_, err = rc.Read(ctx, clusteredEi, map[string]dosa.FieldValue{
   378  		"f1": dosa.FieldValue("key"),
   379  		"c1": dosa.FieldValue(int64(2)),
   380  		"c7": dosa.FieldValue(id)}, dosa.All())
   381  	assert.True(t, dosa.ErrorIsNotFound(err))
   382  
   383  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   384  		return "", "", errors.New("dummy errors")
   385  	}
   386  	rc.PluginFunc = plugin
   387  	// not exist scope, use default
   388  	val, err = rc.Read(ctx, testInfo, map[string]dosa.FieldValue{
   389  		"p1": dosa.FieldValue("data"),
   390  	}, []string{"c1"})
   391  	assert.Contains(t, err.Error(), "dummy errors")
   392  }
   393  
   394  func TestMultiRead(t *testing.T) {
   395  	connectorMap := getConnectorMap()
   396  	rc := NewConnector(cfg, connectorMap, nil)
   397  
   398  	// normal multi-read
   399  	rc.CreateIfNotExists(ctx, testInfoRandom, map[string]dosa.FieldValue{
   400  		"p1": dosa.FieldValue("data"),
   401  		"c1": dosa.FieldValue(int64(1)),
   402  		"c2": dosa.FieldValue(float64(2)),
   403  	})
   404  
   405  	testMultiValues := []map[string]dosa.FieldValue{
   406  		{
   407  			"p1": dosa.FieldValue("data"),
   408  		},
   409  		{
   410  			"c1": dosa.FieldValue(int64(1)),
   411  		},
   412  	}
   413  	_, err := rc.MultiRead(ctx, testInfoRandom, testMultiValues, dosa.All())
   414  	assert.NoError(t, err)
   415  
   416  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   417  		return "", "", errors.New("dummy errors")
   418  	}
   419  	rc.PluginFunc = plugin
   420  	// not exist scope, use default
   421  	_, err = rc.MultiRead(ctx, testInfoRandom, testMultiValues, dosa.All())
   422  	assert.Contains(t, err.Error(), "dummy errors")
   423  }
   424  
   425  func TestConnector_Upsert(t *testing.T) {
   426  	connectorMap := getConnectorMap()
   427  	rc := NewConnector(cfg, connectorMap, nil)
   428  
   429  	// no key value specified
   430  	err := rc.Upsert(ctx, testInfo, map[string]dosa.FieldValue{})
   431  	assert.Error(t, err)
   432  	assert.Contains(t, err.Error(), `partition key "p1"`)
   433  
   434  	testUUIDs := make([]dosa.UUID, 10)
   435  	for x := 0; x < 10; x++ {
   436  		testUUIDs[x] = dosa.NewUUID()
   437  	}
   438  
   439  	// first, insert 10 random UUID values into same partition key
   440  	for x := 0; x < 10; x++ {
   441  		err = rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   442  			"f1": dosa.FieldValue("data"),
   443  			"c1": dosa.FieldValue(int64(1)),
   444  			"c7": dosa.FieldValue(testUUIDs[x])})
   445  		assert.NoError(t, err)
   446  	}
   447  
   448  	// attempt to insert them all again
   449  	for x := 0; x < 10; x++ {
   450  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   451  			"f1": dosa.FieldValue("data"),
   452  			"c1": dosa.FieldValue(int64(1)),
   453  			"c6": dosa.FieldValue(int32(x)),
   454  			"c7": dosa.FieldValue(testUUIDs[x])})
   455  		assert.NoError(t, err)
   456  	}
   457  
   458  	// now, insert them again, but this time with a different secondary key
   459  	for x := 0; x < 10; x++ {
   460  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   461  			"f1": dosa.FieldValue("data"),
   462  			"c1": dosa.FieldValue(int64(2)),
   463  			"c7": dosa.FieldValue(testUUIDs[x])})
   464  		assert.NoError(t, err)
   465  	}
   466  
   467  	// and with a different primary key
   468  	for x := 0; x < 10; x++ {
   469  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   470  			"f1": dosa.FieldValue("different"),
   471  			"c1": dosa.FieldValue(int64(1)),
   472  			"c7": dosa.FieldValue(testUUIDs[x])})
   473  		assert.NoError(t, err)
   474  	}
   475  	data, token, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   476  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   477  	}, dosa.All(), "", 200)
   478  	assert.NoError(t, err)
   479  	assert.Empty(t, token)
   480  	assert.Len(t, data, 20)
   481  	assert.NotNil(t, data[0]["c6"])
   482  
   483  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   484  		return "", "", errors.New("dummy errors")
   485  	}
   486  	rc.PluginFunc = plugin
   487  	// not exist scope, use default
   488  	err = rc.Upsert(ctx, testInfo, map[string]dosa.FieldValue{})
   489  	assert.Contains(t, err.Error(), "dummy errors")
   490  }
   491  
   492  func TestConnector_MultiUpsert(t *testing.T) {
   493  	connectorMap := getConnectorMap()
   494  	rc := NewConnector(cfg, connectorMap, nil)
   495  
   496  	// normal multi-upsert
   497  	testMultiValues := []map[string]dosa.FieldValue{
   498  		{
   499  			"p1": dosa.FieldValue("data"),
   500  		},
   501  		{
   502  			"c1": dosa.FieldValue(int64(1)),
   503  		},
   504  	}
   505  
   506  	_, err := rc.MultiUpsert(ctx, testInfoRandom, testMultiValues)
   507  	assert.NoError(t, err)
   508  
   509  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   510  		return "", "", errors.New("dummy errors")
   511  	}
   512  	rc.PluginFunc = plugin
   513  	// not exist scope, use default
   514  	_, err = rc.MultiUpsert(ctx, testInfoRandom, testMultiValues)
   515  	assert.Contains(t, err.Error(), "dummy errors")
   516  }
   517  
   518  func TestConnector_Remove(t *testing.T) {
   519  	connectorMap := getConnectorMap()
   520  	rc := NewConnector(cfg, connectorMap, nil)
   521  
   522  	// remove with no data
   523  	err := rc.Remove(ctx, testInfo, map[string]dosa.FieldValue{
   524  		"p1": dosa.FieldValue("data")})
   525  	assert.NoError(t, err)
   526  
   527  	// create a single row
   528  	err = rc.CreateIfNotExists(ctx, testInfo, map[string]dosa.FieldValue{
   529  		"p1": dosa.FieldValue("data"),
   530  		"c1": dosa.FieldValue(int64(1)),
   531  		"c2": dosa.FieldValue(float64(2)),
   532  	})
   533  	assert.NoError(t, err)
   534  
   535  	// remove something not there
   536  	err = rc.Remove(ctx, testInfo, map[string]dosa.FieldValue{
   537  		"p1": dosa.FieldValue("nothere")})
   538  	assert.NoError(t, err)
   539  
   540  	// insert into clustered entity
   541  	id := dosa.NewUUID()
   542  	err = rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   543  		"f1": dosa.FieldValue("key"),
   544  		"c1": dosa.FieldValue(int64(1)),
   545  		"c2": dosa.FieldValue(float64(1.2)),
   546  		"c7": dosa.FieldValue(id)})
   547  	assert.NoError(t, err)
   548  
   549  	// remove something not there, but matches partition
   550  	err = rc.Remove(ctx, clusteredEi, map[string]dosa.FieldValue{
   551  		"f1": dosa.FieldValue("key"),
   552  		"c1": dosa.FieldValue(int64(1)),
   553  		"c7": dosa.FieldValue(dosa.NewUUID())})
   554  	assert.NoError(t, err)
   555  
   556  	// and remove the partitioned value
   557  	err = rc.Remove(ctx, clusteredEi, map[string]dosa.FieldValue{
   558  		"f1": dosa.FieldValue("key"),
   559  		"c1": dosa.FieldValue(int64(1)),
   560  		"c7": dosa.FieldValue(id)})
   561  	assert.NoError(t, err)
   562  
   563  	// remove it again, now that there's nothing at all there (corner case)
   564  	err = rc.Remove(ctx, clusteredEi, map[string]dosa.FieldValue{
   565  		"f1": dosa.FieldValue("key"),
   566  		"c1": dosa.FieldValue(int64(1)),
   567  		"c7": dosa.FieldValue(id)})
   568  	assert.NoError(t, err)
   569  
   570  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   571  		return "", "", errors.New("dummy errors")
   572  	}
   573  	rc.PluginFunc = plugin
   574  	// not exist scope, use default
   575  	err = rc.Remove(ctx, testInfo, map[string]dosa.FieldValue{
   576  		"p1": dosa.FieldValue("nothere")})
   577  	assert.Contains(t, err.Error(), "dummy errors")
   578  }
   579  
   580  func TestConnector_RemoveRange(t *testing.T) {
   581  	connectorMap := getConnectorMap()
   582  	rc := NewConnector(cfg, connectorMap, nil)
   583  
   584  	// test removing a range with no data in the range
   585  	err := rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   586  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   587  	})
   588  	assert.NoError(t, err)
   589  
   590  	// insert some data all into the data partition, spread out among the c1 clustering key
   591  	for x := 0; x < idcount; x++ {
   592  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   593  			"f1": dosa.FieldValue("data"),
   594  			"c1": dosa.FieldValue(int64(x)),
   595  			"c7": dosa.FieldValue(dosa.NewUUID())})
   596  		assert.NoError(t, err)
   597  	}
   598  
   599  	// remove with missing primary key values
   600  	err = rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   601  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   602  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   603  	})
   604  	assert.Error(t, err)
   605  	assert.Contains(t, err.Error(), "f1")
   606  	assert.Contains(t, err.Error(), "missing")
   607  
   608  	// delete all values greater than those with 4 for c1
   609  	err = rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   610  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   611  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   612  	})
   613  	assert.NoError(t, err)
   614  
   615  	// ensure all the rows with c1 value less than or equal to 4 still exist
   616  	data, _, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   617  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   618  		"c1": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(int64(4))}},
   619  	}, dosa.All(), "", 200)
   620  	assert.NoError(t, err)
   621  	assert.Len(t, data, idcount/2)
   622  	for i, x := range data {
   623  		assert.Equal(t, x["c1"], int64(i))
   624  	}
   625  
   626  	// ensure all the with a c1 value greater than 4 are deleted.
   627  	data, _, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   628  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   629  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   630  	}, dosa.All(), "", 200)
   631  	assert.NoError(t, err)
   632  	assert.Empty(t, data)
   633  
   634  	// remove everything but the highest value
   635  	err = rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   636  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   637  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(0))}},
   638  	})
   639  	assert.NoError(t, err)
   640  
   641  	// there should only be one value left now
   642  	data, _, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   643  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   644  	}, dosa.All(), "", 200)
   645  	assert.NoError(t, err)
   646  	assert.Len(t, data, 1)
   647  
   648  	// test completely deleting all the rows in a partition.
   649  	err = rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   650  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   651  	})
   652  	assert.NoError(t, err)
   653  	data, _, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   654  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   655  	}, dosa.All(), "", 200)
   656  	assert.NoError(t, err)
   657  	assert.Empty(t, data)
   658  
   659  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   660  		return "", "", errors.New("dummy errors")
   661  	}
   662  	rc.PluginFunc = plugin
   663  	// not exist scope, use default
   664  	err = rc.RemoveRange(ctx, clusteredEi, map[string][]*dosa.Condition{
   665  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   666  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(0))}},
   667  	})
   668  	assert.Contains(t, err.Error(), "dummy errors")
   669  }
   670  
   671  func TestConnector_MultiRemove(t *testing.T) {
   672  	connectorMap := getConnectorMap()
   673  	rc := NewConnector(cfg, connectorMap, nil)
   674  
   675  	// normal multi-upsert
   676  	testMultiValues := []map[string]dosa.FieldValue{
   677  		{
   678  			"p1": dosa.FieldValue("data"),
   679  		},
   680  		{
   681  			"c1": dosa.FieldValue(int64(1)),
   682  		},
   683  	}
   684  
   685  	errs, err := rc.MultiRemove(ctx, testInfoRandom, testMultiValues)
   686  	assert.NoError(t, err)
   687  	assert.NotNil(t, errs)
   688  
   689  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   690  		return "", "", errors.New("dummy errors")
   691  	}
   692  	rc.PluginFunc = plugin
   693  	// not exist scope, use default
   694  	errs, err = rc.MultiRemove(ctx, testInfoRandom, testMultiValues)
   695  	assert.Contains(t, err.Error(), "dummy errors")
   696  }
   697  
   698  type ByUUID []dosa.UUID
   699  
   700  func (u ByUUID) Len() int           { return len(u) }
   701  func (u ByUUID) Swap(i, j int)      { u[i], u[j] = u[j], u[i] }
   702  func (u ByUUID) Less(i, j int) bool { return string(u[i]) > string(u[j]) }
   703  
   704  func TestConnector_Range(t *testing.T) {
   705  	connectorMap := getConnectorMap()
   706  	rc := NewConnector(cfg, connectorMap, nil)
   707  
   708  	// no data at all (corner case)
   709  	data, token, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   710  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   711  	}, dosa.All(), "", 200)
   712  	assert.NoError(t, err)
   713  	assert.Empty(t, token)
   714  	assert.Empty(t, data)
   715  
   716  	// insert some data into data/1/uuid with a random set of uuids
   717  	// we insert them in a random order
   718  	testUUIDs := make([]dosa.UUID, idcount)
   719  	for x := 0; x < idcount; x++ {
   720  		testUUIDs[x] = dosa.NewUUID()
   721  	}
   722  	for x := 0; x < idcount; x++ {
   723  		err := rc.CreateIfNotExists(ctx, clusteredEi, map[string]dosa.FieldValue{
   724  			"f1": dosa.FieldValue("data"),
   725  			"c1": dosa.FieldValue(int64(1)),
   726  			"c7": dosa.FieldValue(testUUIDs[x])})
   727  		assert.NoError(t, err)
   728  	}
   729  
   730  	// search using a different partition key
   731  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   732  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("wrongdata")}},
   733  	}, dosa.All(), "", 200)
   734  	assert.NoError(t, err)
   735  	assert.Empty(t, data)
   736  
   737  	sort.Sort(ByUUID(testUUIDs))
   738  	// search using the right partition key, and check that the data was insertion-sorted
   739  	// correctly
   740  	data, _, _ = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   741  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   742  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   743  	}, dosa.All(), "", 200)
   744  	for idx, row := range data {
   745  		assert.Equal(t, testUUIDs[idx], row["c7"])
   746  	}
   747  
   748  	// find the midpoint and look for all values greater than that
   749  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   750  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   751  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   752  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}},
   753  	}, dosa.All(), "", 200)
   754  	assert.NoError(t, err)
   755  	assert.Len(t, data, idcount/2-1)
   756  
   757  	// there's one more for greater than or equal
   758  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   759  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   760  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   761  		"c7": {{Op: dosa.GtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}},
   762  	}, dosa.All(), "", 200)
   763  	assert.NoError(t, err)
   764  	assert.Len(t, data, idcount/2)
   765  
   766  	// find the midpoint and look for all values less than that
   767  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   768  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   769  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   770  		"c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount/2])}},
   771  	}, dosa.All(), "", 200)
   772  	assert.NoError(t, err)
   773  	assert.Len(t, data, idcount/2-1)
   774  
   775  	// and same for less than or equal
   776  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   777  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   778  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   779  		"c7": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2])}},
   780  	}, dosa.All(), "", 200)
   781  	assert.NoError(t, err)
   782  	assert.Len(t, data, idcount/2)
   783  
   784  	// look off the end of the left side, so greater than maximum (edge case)
   785  	// (uuids are ordered descending so this is non-intuitively backwards)
   786  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   787  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   788  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   789  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[0])}},
   790  	}, dosa.All(), "", 200)
   791  	assert.NoError(t, err)
   792  	assert.Empty(t, data)
   793  
   794  	// look off the end of the left side, so greater than maximum
   795  	data, _, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   796  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   797  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   798  		"c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount-1])}},
   799  	}, dosa.All(), "", 200)
   800  	assert.NoError(t, err)
   801  	assert.Empty(t, data)
   802  
   803  	// Test Ranging on an Index
   804  
   805  	// Get "1" partition
   806  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   807  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   808  	}, dosa.All(), "", 200)
   809  	assert.NoError(t, err)
   810  	assert.Len(t, data, 10)
   811  	assert.Empty(t, token)
   812  
   813  	// Get the "2" partition, should be empty
   814  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   815  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(2))}},
   816  	}, dosa.All(), "", 200)
   817  	assert.NoError(t, err)
   818  	assert.Empty(t, data)
   819  	assert.Empty(t, token)
   820  
   821  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   822  		return "", "", errors.New("dummy errors")
   823  	}
   824  	rc.PluginFunc = plugin
   825  	// not exist scope, use default
   826  	data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   827  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   828  	}, dosa.All(), "", 200)
   829  	assert.Contains(t, err.Error(), "dummy errors")
   830  }
   831  
   832  func TestConnector_Scan(t *testing.T) {
   833  	connectorMap := getConnectorMap()
   834  	rc := NewConnector(cfg, connectorMap, nil)
   835  
   836  	testUUIDs := make([]dosa.UUID, idcount)
   837  	for x := 0; x < idcount; x++ {
   838  		testUUIDs[x] = dosa.NewUUID()
   839  	}
   840  	// scan with nothing there yet
   841  	_, token, err := rc.Scan(ctx, clusteredEi, dosa.All(), "", 100)
   842  	assert.NoError(t, err)
   843  	assert.Empty(t, token)
   844  
   845  	// first, insert some random UUID values into two partition keys
   846  	for x := 0; x < idcount; x++ {
   847  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   848  			"f1": dosa.FieldValue("data" + string(x%2)),
   849  			"c1": dosa.FieldValue(int64(1)),
   850  			"c7": dosa.FieldValue(testUUIDs[x])})
   851  		assert.NoError(t, err)
   852  	}
   853  
   854  	data, token, err := rc.Scan(ctx, clusteredEi, dosa.All(), "", 100)
   855  	assert.NoError(t, err)
   856  	assert.Len(t, data, idcount)
   857  	assert.Empty(t, token)
   858  
   859  	// there's an odd edge case when you delete everything, so do that, then call scan
   860  	for x := 0; x < idcount; x++ {
   861  		err := rc.Remove(ctx, clusteredEi, map[string]dosa.FieldValue{
   862  			"f1": dosa.FieldValue("data" + string(x%2)),
   863  			"c1": dosa.FieldValue(int64(1)),
   864  			"c7": dosa.FieldValue(testUUIDs[x])})
   865  		assert.NoError(t, err)
   866  	}
   867  	data, token, err = rc.Scan(ctx, clusteredEi, dosa.All(), "", 100)
   868  	assert.NoError(t, err)
   869  	assert.Empty(t, data)
   870  	assert.Empty(t, token)
   871  
   872  	plugin := func(scope, namePrefix, opName string) (string, string, error) {
   873  		return "", "", errors.New("dummy errors")
   874  	}
   875  	rc.PluginFunc = plugin
   876  	// not exist scope, use default
   877  	data, token, err = rc.Scan(ctx, clusteredEi, dosa.All(), "", 100)
   878  	assert.Contains(t, err.Error(), "dummy errors")
   879  }
   880  
   881  func TestConnector_Shutdown(t *testing.T) {
   882  	connectorMap := getConnectorMap()
   883  	rc := NewConnector(cfg, connectorMap, nil)
   884  
   885  	err := rc.Shutdown()
   886  	assert.NoError(t, err)
   887  }
   888  
   889  func TestConnector_TimeUUIDs(t *testing.T) {
   890  	connectorMap := getConnectorMap()
   891  	rc := NewConnector(cfg, connectorMap, nil)
   892  
   893  	// insert a bunch of values with V1 timestamps as clustering keys
   894  	for x := 0; x < idcount; x++ {
   895  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   896  			"f1": dosa.FieldValue("data"),
   897  			"c1": dosa.FieldValue(int64(1)),
   898  			"c6": dosa.FieldValue(int32(x)),
   899  			"c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))})
   900  		assert.NoError(t, err)
   901  	}
   902  
   903  	// read them back, they should be in reverse order
   904  	data, _, _ := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   905  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   906  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   907  	}, dosa.All(), "", 200)
   908  
   909  	// check that the order is backwards
   910  	for idx, row := range data {
   911  		assert.Equal(t, int32(idcount-idx-1), row["c6"])
   912  	}
   913  
   914  	// now mix in a few V4 UUIDs
   915  	for x := 0; x < idcount; x++ {
   916  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
   917  			"f1": dosa.FieldValue("data"),
   918  			"c1": dosa.FieldValue(int64(1)),
   919  			"c6": dosa.FieldValue(int32(idcount + x)),
   920  			"c7": dosa.FieldValue(dosa.NewUUID())})
   921  		assert.NoError(t, err)
   922  	}
   923  
   924  	// the V4's should all be first, since V4 UUIDs sort > V1 UUIDs
   925  	data, _, _ = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
   926  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   927  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   928  	}, dosa.All(), "", 200)
   929  	for _, row := range data[0:idcount] {
   930  		assert.True(t, row["c6"].(int32) >= idcount, row["c6"])
   931  	}
   932  }
   933  
   934  func TestConnector_ScanWithToken(t *testing.T) {
   935  	connectorMap := getConnectorMap()
   936  	rc := NewConnector(cfg, connectorMap, nil)
   937  
   938  	createTestData(t, rc, func(id int) string {
   939  		return "data" + string(id%3)
   940  	}, idcount)
   941  	var token string
   942  	var err error
   943  	var data []map[string]dosa.FieldValue
   944  	for x := 0; x < idcount; x++ {
   945  		data, token, err = rc.Scan(ctx, clusteredEi, dosa.All(), token, 1)
   946  		assert.NoError(t, err)
   947  		assert.Len(t, data, 1)
   948  		if x < idcount-1 {
   949  			assert.NotEmpty(t, token)
   950  		} else {
   951  			assert.Empty(t, token)
   952  		}
   953  	}
   954  	// now walk through again, but delete the item returned
   955  	// (note: we don't have to reset token, because it should be empty now)
   956  	for x := 0; x < idcount; x++ {
   957  		data, token, err = rc.Scan(ctx, clusteredEi, dosa.All(), token, 1)
   958  		assert.NoError(t, err)
   959  		assert.Len(t, data, 1)
   960  		if x < idcount-1 {
   961  			assert.NotEmpty(t, token)
   962  		} else {
   963  			assert.Empty(t, token)
   964  		}
   965  		err = rc.Remove(ctx, clusteredEi, data[0])
   966  		assert.NoError(t, err)
   967  	}
   968  }
   969  
   970  func TestConnector_ScanWithTokenFromWrongTable(t *testing.T) {
   971  	connectorMap := getConnectorMap()
   972  	rc := NewConnector(cfg, connectorMap, nil)
   973  
   974  	createTestData(t, rc, func(id int) string {
   975  		return "data" + string(id%3)
   976  	}, idcount)
   977  	err := rc.Upsert(ctx, testInfo, map[string]dosa.FieldValue{
   978  		"p1": dosa.FieldValue("test"),
   979  	})
   980  	assert.NoError(t, err)
   981  
   982  	// get a token from one table
   983  	_, token, err := rc.Scan(ctx, clusteredEi, dosa.All(), "", 1)
   984  	assert.NoError(t, err)
   985  	assert.NotEmpty(t, token)
   986  
   987  	// now use it for another scan (oops)
   988  	_, _, err = rc.Scan(ctx, testInfo, dosa.All(), token, 1)
   989  	assert.Error(t, err)
   990  	assert.Contains(t, err.Error(), "Invalid token")
   991  	assert.Contains(t, err.Error(), "Missing value")
   992  }
   993  
   994  func TestConnector_ScanWithTokenNoClustering(t *testing.T) {
   995  	connectorMap := getConnectorMap()
   996  	rc := NewConnector(cfg, connectorMap, nil)
   997  
   998  	for x := 0; x < idcount; x++ {
   999  		rc.Upsert(ctx, testInfo, map[string]dosa.FieldValue{
  1000  			"p1": dosa.FieldValue("data" + string(x)),
  1001  		})
  1002  	}
  1003  	var token string
  1004  	var err error
  1005  	var data []map[string]dosa.FieldValue
  1006  	for x := 0; x < idcount; x++ {
  1007  		data, token, err = rc.Scan(ctx, testInfo, dosa.All(), token, 1)
  1008  		assert.NoError(t, err)
  1009  		assert.Len(t, data, 1)
  1010  		if x < idcount-1 {
  1011  			assert.NotEmpty(t, token)
  1012  		} else {
  1013  			assert.Empty(t, token)
  1014  		}
  1015  	}
  1016  	// and again, this time deleting as we go
  1017  	for x := 0; x < idcount; x++ {
  1018  		data, token, err = rc.Scan(ctx, testInfo, dosa.All(), token, 1)
  1019  		assert.NoError(t, err)
  1020  		assert.Len(t, data, 1)
  1021  		if x < idcount-1 {
  1022  			assert.NotEmpty(t, token)
  1023  		} else {
  1024  			assert.Empty(t, token)
  1025  		}
  1026  		err = rc.Remove(ctx, testInfo, data[0])
  1027  		assert.NoError(t, err)
  1028  	}
  1029  }
  1030  
  1031  func TestRangePager(t *testing.T) {
  1032  	connectorMap := getConnectorMap()
  1033  	rc := NewConnector(cfg, connectorMap, nil)
  1034  
  1035  	// create test data in one partition "data"
  1036  	createTestData(t, rc, func(_ int) string { return "data" }, idcount)
  1037  	var token string
  1038  	var err error
  1039  	var data []map[string]dosa.FieldValue
  1040  	for x := 0; x < idcount; x++ {
  1041  		data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
  1042  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
  1043  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
  1044  		}, dosa.All(), token, 1)
  1045  		assert.NoError(t, err)
  1046  		assert.Len(t, data, 1)
  1047  		if x < idcount-1 {
  1048  			assert.NotEmpty(t, token)
  1049  		} else {
  1050  			assert.Empty(t, token)
  1051  		}
  1052  	}
  1053  	// now walk through again, but delete the item returned
  1054  	// (note: we don't have to reset token, because it should be empty now)
  1055  	for x := 0; x < idcount; x++ {
  1056  		data, token, err = rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
  1057  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
  1058  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
  1059  		}, dosa.All(), token, 1)
  1060  		assert.NoError(t, err)
  1061  		assert.Len(t, data, 1)
  1062  		if x < idcount-1 {
  1063  			assert.NotEmpty(t, token)
  1064  		} else {
  1065  			assert.Empty(t, token)
  1066  		}
  1067  		err = rc.Remove(ctx, clusteredEi, data[0])
  1068  		assert.NoError(t, err)
  1069  	}
  1070  }
  1071  
  1072  func TestInvalidToken(t *testing.T) {
  1073  	connectorMap := getConnectorMap()
  1074  	rc := NewConnector(cfg, connectorMap, nil)
  1075  
  1076  	// we don't use the token if there's no data that matches, so lets
  1077  	// create one row
  1078  	createTestData(t, rc, func(id int) string {
  1079  		return "data"
  1080  	}, 1)
  1081  
  1082  	token := "this is not a token and not a hot dog"
  1083  	t.Run("testInvalidTokenRange", func(t *testing.T) {
  1084  		_, _, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
  1085  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
  1086  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
  1087  		}, dosa.All(), token, 1)
  1088  		assert.Error(t, err)
  1089  		assert.Contains(t, err.Error(), "Invalid token")
  1090  	})
  1091  	t.Run("testInvalidTokenScan", func(t *testing.T) {
  1092  		_, _, err := rc.Scan(ctx, clusteredEi, dosa.All(), token, 1)
  1093  		assert.Error(t, err)
  1094  		assert.Contains(t, err.Error(), "Invalid token")
  1095  	})
  1096  }
  1097  
  1098  func TestConnector_RangeWithBadCriteria(t *testing.T) {
  1099  	connectorMap := getConnectorMap()
  1100  	rc := NewConnector(cfg, connectorMap, nil)
  1101  
  1102  	// we don't look at the criteria unless there is at least one row
  1103  	createTestData(t, rc, func(id int) string {
  1104  		return "data"
  1105  	}, 1)
  1106  
  1107  	_, _, err := rc.Range(ctx, clusteredEi, map[string][]*dosa.Condition{
  1108  		"c2": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
  1109  		"c3": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
  1110  	}, dosa.All(), "", 1)
  1111  	assert.Error(t, err)
  1112  }
  1113  
  1114  // createTestData populates some test data. The keyGenFunc can either return a constant,
  1115  // which gives you a single partition of data, or some function of the current offset, which
  1116  // will scatter the data across different partition keys
  1117  func createTestData(t *testing.T, rc *Connector, keyGenFunc func(int) string, idcount int) {
  1118  	// insert a bunch of values with V1 timestamps as clustering keys
  1119  	for x := 0; x < idcount; x++ {
  1120  		err := rc.Upsert(ctx, clusteredEi, map[string]dosa.FieldValue{
  1121  			"f1": dosa.FieldValue(keyGenFunc(x)),
  1122  			"c1": dosa.FieldValue(int64(1)),
  1123  			"c6": dosa.FieldValue(int32(x)),
  1124  			"c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))})
  1125  		assert.NoError(t, err)
  1126  	}
  1127  }