github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/memory/memory_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 memory
    22  
    23  import (
    24  	"context"
    25  	"testing"
    26  	"time"
    27  
    28  	"sort"
    29  
    30  	"github.com/satori/go.uuid"
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/uber-go/dosa"
    33  )
    34  
    35  var testSchemaRef = dosa.SchemaRef{
    36  	Scope:      "scope1",
    37  	NamePrefix: "namePrefix",
    38  	EntityName: "eName",
    39  	Version:    12345,
    40  }
    41  
    42  var testEi = &dosa.EntityInfo{
    43  	Ref: &testSchemaRef,
    44  	Def: &dosa.EntityDefinition{
    45  		Columns: []*dosa.ColumnDefinition{
    46  			{Name: "p1", Type: dosa.String},
    47  			{Name: "c1", Type: dosa.Int64},
    48  			{Name: "c2", Type: dosa.Double},
    49  			{Name: "c3", Type: dosa.String},
    50  			{Name: "c4", Type: dosa.Blob},
    51  			{Name: "c5", Type: dosa.Bool},
    52  			{Name: "c6", Type: dosa.Int32},
    53  			{Name: "c7", Type: dosa.TUUID},
    54  		},
    55  		Key: &dosa.PrimaryKey{
    56  			PartitionKeys: []string{"p1"},
    57  		},
    58  		Name: "t1",
    59  		Indexes: map[string]*dosa.IndexDefinition{
    60  			"i1": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
    61  	},
    62  }
    63  var clusteredEi = &dosa.EntityInfo{
    64  	Ref: &testSchemaRef,
    65  	Def: &dosa.EntityDefinition{
    66  		Columns: []*dosa.ColumnDefinition{
    67  			{Name: "f1", Type: dosa.String},
    68  			{Name: "c1", Type: dosa.Int64},
    69  			{Name: "c2", Type: dosa.Double},
    70  			{Name: "c3", Type: dosa.String},
    71  			{Name: "c4", Type: dosa.Blob},
    72  			{Name: "c5", Type: dosa.Bool},
    73  			{Name: "c6", Type: dosa.Int32},
    74  			{Name: "c7", Type: dosa.TUUID},
    75  		},
    76  		Key: &dosa.PrimaryKey{
    77  			PartitionKeys: []string{"f1"},
    78  			ClusteringKeys: []*dosa.ClusteringKey{
    79  				{Name: "c1", Descending: false},
    80  				{Name: "c7", Descending: true},
    81  			},
    82  		},
    83  		Name: "t2",
    84  		Indexes: map[string]*dosa.IndexDefinition{
    85  			"i2": {Key: &dosa.PrimaryKey{PartitionKeys: []string{"c1"}}}},
    86  	},
    87  }
    88  
    89  func TestConnector_CreateIfNotExists(t *testing.T) {
    90  	sut := NewConnector()
    91  
    92  	err := sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{
    93  		"p1": dosa.FieldValue("data"),
    94  	})
    95  	assert.NoError(t, err)
    96  
    97  	err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{
    98  		"p1": dosa.FieldValue("data"),
    99  	})
   100  
   101  	assert.True(t, dosa.ErrorIsAlreadyExists(err))
   102  }
   103  func TestConnector_Upsert(t *testing.T) {
   104  	sut := NewConnector()
   105  
   106  	// no key value specified
   107  	err := sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{})
   108  	assert.Error(t, err)
   109  	assert.Contains(t, err.Error(), `partition key "p1"`)
   110  
   111  	// regular upsert
   112  	err = sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{
   113  		"p1": dosa.FieldValue("data"),
   114  	})
   115  	assert.NoError(t, err)
   116  	vals, err := sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   117  		"p1": dosa.FieldValue("data")}, []string{"c1"})
   118  	assert.NoError(t, err)
   119  	assert.Nil(t, vals["c1"])
   120  
   121  	err = sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{
   122  		"p1": dosa.FieldValue("data"),
   123  		"c1": dosa.FieldValue(int64(1)),
   124  	})
   125  	assert.NoError(t, err)
   126  
   127  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   128  		"p1": dosa.FieldValue("data")}, []string{"c1"})
   129  	assert.NoError(t, err)
   130  	assert.Equal(t, dosa.FieldValue(int64(1)), vals["c1"])
   131  }
   132  
   133  func TestConnector_Read(t *testing.T) {
   134  	sut := NewConnector()
   135  
   136  	// read with no data
   137  	vals, err := sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   138  		"p1": dosa.FieldValue("data")}, []string{"c1"})
   139  	assert.True(t, dosa.ErrorIsNotFound(err))
   140  
   141  	// read with no key field
   142  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{}, dosa.All())
   143  	assert.Error(t, err)
   144  	assert.Contains(t, err.Error(), `partition key "p1"`)
   145  
   146  	err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{
   147  		"p1": dosa.FieldValue("data"),
   148  		"c1": dosa.FieldValue(int64(1)),
   149  		"c2": dosa.FieldValue(float64(2)),
   150  	})
   151  	assert.NoError(t, err)
   152  
   153  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   154  		"p1": dosa.FieldValue("data")}, []string{"c1"})
   155  	assert.NoError(t, err)
   156  	assert.Equal(t, int64(1), vals["c1"])
   157  
   158  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   159  		"p1": dosa.FieldValue("data")}, dosa.All())
   160  	assert.NoError(t, err)
   161  	assert.Equal(t, int64(1), vals["c1"])
   162  	assert.Equal(t, float64(2), vals["c2"])
   163  	assert.Equal(t, "data", vals["p1"])
   164  
   165  	// read a key that isn't there
   166  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   167  		"p1": dosa.FieldValue("not there")}, dosa.All())
   168  	assert.True(t, dosa.ErrorIsNotFound(err))
   169  
   170  	// now delete the one that is
   171  	err = sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{
   172  		"p1": dosa.FieldValue("data")})
   173  	assert.NoError(t, err)
   174  
   175  	// read the deleted key
   176  	vals, err = sut.Read(context.TODO(), testEi, map[string]dosa.FieldValue{
   177  		"p1": dosa.FieldValue("data")}, dosa.All())
   178  	assert.True(t, dosa.ErrorIsNotFound(err))
   179  
   180  	// insert into clustered entity
   181  	id := dosa.NewUUID()
   182  	err = sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   183  		"f1": dosa.FieldValue("key"),
   184  		"c1": dosa.FieldValue(int64(1)),
   185  		"c2": dosa.FieldValue(float64(1.2)),
   186  		"c7": dosa.FieldValue(id)})
   187  	assert.NoError(t, err)
   188  
   189  	// read that row
   190  	vals, err = sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   191  		"f1": dosa.FieldValue("key"),
   192  		"c1": dosa.FieldValue(int64(1)),
   193  		"c7": dosa.FieldValue(id)}, dosa.All())
   194  	assert.NoError(t, err)
   195  	assert.Equal(t, dosa.FieldValue(float64(1.2)), vals["c2"])
   196  
   197  	// and fail a read on a clustered key
   198  	_, err = sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   199  		"f1": dosa.FieldValue("key"),
   200  		"c1": dosa.FieldValue(int64(2)),
   201  		"c7": dosa.FieldValue(id)}, dosa.All())
   202  	assert.True(t, dosa.ErrorIsNotFound(err))
   203  }
   204  
   205  func TestConnector_Remove(t *testing.T) {
   206  	sut := NewConnector()
   207  
   208  	// remove with no data
   209  	err := sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{
   210  		"p1": dosa.FieldValue("data")})
   211  	assert.NoError(t, err)
   212  
   213  	// create a single row
   214  	err = sut.CreateIfNotExists(context.TODO(), testEi, map[string]dosa.FieldValue{
   215  		"p1": dosa.FieldValue("data"),
   216  		"c1": dosa.FieldValue(int64(1)),
   217  		"c2": dosa.FieldValue(float64(2)),
   218  	})
   219  	assert.NoError(t, err)
   220  
   221  	// remove something not there
   222  	err = sut.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{
   223  		"p1": dosa.FieldValue("nothere")})
   224  	assert.NoError(t, err)
   225  
   226  	// insert into clustered entity
   227  	id := dosa.NewUUID()
   228  	err = sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   229  		"f1": dosa.FieldValue("key"),
   230  		"c1": dosa.FieldValue(int64(1)),
   231  		"c2": dosa.FieldValue(float64(1.2)),
   232  		"c7": dosa.FieldValue(id)})
   233  	assert.NoError(t, err)
   234  
   235  	// remove something not there, but matches partition
   236  	err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   237  		"f1": dosa.FieldValue("key"),
   238  		"c1": dosa.FieldValue(int64(1)),
   239  		"c7": dosa.FieldValue(dosa.NewUUID())})
   240  	assert.NoError(t, err)
   241  
   242  	// and remove the partitioned value
   243  	err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   244  		"f1": dosa.FieldValue("key"),
   245  		"c1": dosa.FieldValue(int64(1)),
   246  		"c7": dosa.FieldValue(id)})
   247  	assert.NoError(t, err)
   248  
   249  	// remove it again, now that there's nothing at all there (corner case)
   250  	err = sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   251  		"f1": dosa.FieldValue("key"),
   252  		"c1": dosa.FieldValue(int64(1)),
   253  		"c7": dosa.FieldValue(id)})
   254  	assert.NoError(t, err)
   255  }
   256  
   257  func TestConnector_RemoveRange(t *testing.T) {
   258  	const idcount = 10
   259  	sut := NewConnector()
   260  
   261  	// test removing a range with no data in the range
   262  	err := sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   263  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   264  	})
   265  	assert.NoError(t, err)
   266  
   267  	// insert some data all into the data partition, spread out among the c1 clustering key
   268  	for x := 0; x < idcount; x++ {
   269  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   270  			"f1": dosa.FieldValue("data"),
   271  			"c1": dosa.FieldValue(int64(x)),
   272  			"c7": dosa.FieldValue(dosa.NewUUID())})
   273  		assert.NoError(t, err)
   274  	}
   275  
   276  	// remove with missing primary key values
   277  	err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   278  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   279  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   280  	})
   281  	assert.Error(t, err)
   282  	assert.Contains(t, err.Error(), "f1")
   283  	assert.Contains(t, err.Error(), "missing")
   284  
   285  	// delete all values greater than those with 4 for c1
   286  	err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   287  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   288  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   289  	})
   290  	assert.NoError(t, err)
   291  
   292  	// ensure all the rows with c1 value less than or equal to 4 still exist
   293  	data, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   294  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   295  		"c1": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(int64(4))}},
   296  	}, dosa.All(), "", 200)
   297  	assert.NoError(t, err)
   298  	assert.Len(t, data, idcount/2)
   299  	for i, x := range data {
   300  		assert.Equal(t, x["c1"], int64(i))
   301  	}
   302  
   303  	// ensure all the with a c1 value greater than 4 are deleted.
   304  	data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   305  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   306  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(4))}},
   307  	}, dosa.All(), "", 200)
   308  	assert.NoError(t, err)
   309  	assert.Empty(t, data)
   310  
   311  	// remove everything but the highest value
   312  	err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   313  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   314  		"c1": {{Op: dosa.Gt, Value: dosa.FieldValue(int64(0))}},
   315  	})
   316  	assert.NoError(t, err)
   317  
   318  	// there should only be one value left now
   319  	data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   320  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   321  	}, dosa.All(), "", 200)
   322  	assert.NoError(t, err)
   323  	assert.Len(t, data, 1)
   324  
   325  	// test completely deleting all the rows in a partition.
   326  	err = sut.RemoveRange(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   327  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   328  	})
   329  	assert.NoError(t, err)
   330  	data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   331  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   332  	}, dosa.All(), "", 200)
   333  	assert.NoError(t, err)
   334  	assert.Empty(t, data)
   335  }
   336  
   337  func TestConnector_Shutdown(t *testing.T) {
   338  	sut := NewConnector()
   339  
   340  	err := sut.Shutdown()
   341  	assert.NoError(t, err)
   342  	assert.Nil(t, sut.data)
   343  }
   344  
   345  // test CreateIfNotExists with partitioning
   346  func TestConnector_CreateIfNotExists2(t *testing.T) {
   347  	sut := NewConnector()
   348  
   349  	testUUIDs := make([]dosa.UUID, 10)
   350  	for x := 0; x < 10; x++ {
   351  		testUUIDs[x] = dosa.NewUUID()
   352  	}
   353  
   354  	// first, insert 10 random UUID values into same partition key
   355  	for x := 0; x < 10; x++ {
   356  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   357  			"f1": dosa.FieldValue("data"),
   358  			"c1": dosa.FieldValue(int64(1)),
   359  			"c7": dosa.FieldValue(testUUIDs[x])})
   360  		assert.NoError(t, err)
   361  	}
   362  	// attempt to insert them all again
   363  	for x := 0; x < 10; x++ {
   364  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   365  			"f1": dosa.FieldValue("data"),
   366  			"c1": dosa.FieldValue(int64(1)),
   367  			"c7": dosa.FieldValue(testUUIDs[x])})
   368  		assert.Error(t, err, string(testUUIDs[x]))
   369  		assert.True(t, dosa.ErrorIsAlreadyExists(err))
   370  	}
   371  	// now, insert them again, but this time with a different secondary key
   372  	for x := 0; x < 10; x++ {
   373  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   374  			"f1": dosa.FieldValue("data"),
   375  			"c1": dosa.FieldValue(int64(2)),
   376  			"c7": dosa.FieldValue(testUUIDs[x])})
   377  		assert.NoError(t, err)
   378  	}
   379  	// and with a different primary key
   380  	for x := 0; x < 10; x++ {
   381  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   382  			"f1": dosa.FieldValue("different"),
   383  			"c1": dosa.FieldValue(int64(1)),
   384  			"c7": dosa.FieldValue(testUUIDs[x])})
   385  		assert.NoError(t, err)
   386  	}
   387  	data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   388  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   389  	}, dosa.All(), "", 200)
   390  	assert.NoError(t, err)
   391  	assert.Empty(t, token)
   392  	assert.Len(t, data, 20)
   393  }
   394  
   395  func TestConnector_Upsert2(t *testing.T) {
   396  	sut := NewConnector()
   397  
   398  	testUUIDs := make([]dosa.UUID, 10)
   399  	for x := 0; x < 10; x++ {
   400  		testUUIDs[x] = dosa.NewUUID()
   401  	}
   402  
   403  	// first, insert 10 random UUID values into same partition key
   404  	for x := 0; x < 10; x++ {
   405  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   406  			"f1": dosa.FieldValue("data"),
   407  			"c1": dosa.FieldValue(int64(1)),
   408  			"c7": dosa.FieldValue(testUUIDs[x])})
   409  		assert.NoError(t, err)
   410  	}
   411  	// attempt to insert them all again
   412  	for x := 0; x < 10; x++ {
   413  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   414  			"f1": dosa.FieldValue("data"),
   415  			"c1": dosa.FieldValue(int64(1)),
   416  			"c6": dosa.FieldValue(int32(x)),
   417  			"c7": dosa.FieldValue(testUUIDs[x])})
   418  		assert.NoError(t, err)
   419  	}
   420  	// now, insert them again, but this time with a different secondary key
   421  	for x := 0; x < 10; x++ {
   422  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   423  			"f1": dosa.FieldValue("data"),
   424  			"c1": dosa.FieldValue(int64(2)),
   425  			"c7": dosa.FieldValue(testUUIDs[x])})
   426  		assert.NoError(t, err)
   427  	}
   428  	// and with a different primary key
   429  	for x := 0; x < 10; x++ {
   430  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   431  			"f1": dosa.FieldValue("different"),
   432  			"c1": dosa.FieldValue(int64(1)),
   433  			"c7": dosa.FieldValue(testUUIDs[x])})
   434  		assert.NoError(t, err)
   435  	}
   436  	data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   437  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   438  	}, dosa.All(), "", 200)
   439  	assert.NoError(t, err)
   440  	assert.Empty(t, token)
   441  	assert.Len(t, data, 20)
   442  	assert.NotNil(t, data[0]["c6"])
   443  }
   444  
   445  func TestConnector_Range(t *testing.T) {
   446  	const idcount = 10
   447  	sut := NewConnector()
   448  
   449  	// no data at all (corner case)
   450  	data, token, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   451  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   452  	}, dosa.All(), "", 200)
   453  	assert.NoError(t, err)
   454  	assert.Empty(t, token)
   455  	assert.Empty(t, data)
   456  
   457  	// insert some data into data/1/uuid with a random set of uuids
   458  	// we insert them in a random order
   459  	testUUIDs := make([]dosa.UUID, idcount)
   460  	for x := 0; x < idcount; x++ {
   461  		testUUIDs[x] = dosa.NewUUID()
   462  	}
   463  	for x := 0; x < idcount; x++ {
   464  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   465  			"f1": dosa.FieldValue("data"),
   466  			"c1": dosa.FieldValue(int64(1)),
   467  			"c7": dosa.FieldValue(testUUIDs[x])})
   468  		assert.NoError(t, err)
   469  	}
   470  
   471  	// search using a different partition key
   472  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   473  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("wrongdata")}},
   474  	}, dosa.All(), "", 200)
   475  	assert.NoError(t, err)
   476  	assert.Empty(t, data)
   477  
   478  	sort.Sort(ByUUID(testUUIDs))
   479  	// search using the right partition key, and check that the data was insertion-sorted
   480  	// correctly
   481  	data, _, _ = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   482  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   483  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   484  	}, dosa.All(), "", 200)
   485  	for idx, row := range data {
   486  		assert.Equal(t, testUUIDs[idx], row["c7"])
   487  	}
   488  
   489  	// find the midpoint and look for all values greater than that
   490  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   491  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   492  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   493  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}},
   494  	}, dosa.All(), "", 200)
   495  	assert.NoError(t, err)
   496  	assert.Len(t, data, idcount/2-1)
   497  
   498  	// there's one more for greater than or equal
   499  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   500  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   501  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   502  		"c7": {{Op: dosa.GtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2-1])}},
   503  	}, dosa.All(), "", 200)
   504  	assert.NoError(t, err)
   505  	assert.Len(t, data, idcount/2)
   506  
   507  	// find the midpoint and look for all values less than that
   508  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   509  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   510  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   511  		"c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount/2])}},
   512  	}, dosa.All(), "", 200)
   513  	assert.NoError(t, err)
   514  	assert.Len(t, data, idcount/2-1)
   515  
   516  	// and same for less than or equal
   517  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   518  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   519  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   520  		"c7": {{Op: dosa.LtOrEq, Value: dosa.FieldValue(testUUIDs[idcount/2])}},
   521  	}, dosa.All(), "", 200)
   522  	assert.NoError(t, err)
   523  	assert.Len(t, data, idcount/2)
   524  
   525  	// look off the end of the left side, so greater than maximum (edge case)
   526  	// (uuids are ordered descending so this is non-intuitively backwards)
   527  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   528  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   529  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   530  		"c7": {{Op: dosa.Gt, Value: dosa.FieldValue(testUUIDs[0])}},
   531  	}, dosa.All(), "", 200)
   532  	assert.NoError(t, err)
   533  	assert.Empty(t, data)
   534  
   535  	// look off the end of the left side, so greater than maximum
   536  	data, _, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   537  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   538  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   539  		"c7": {{Op: dosa.Lt, Value: dosa.FieldValue(testUUIDs[idcount-1])}},
   540  	}, dosa.All(), "", 200)
   541  	assert.NoError(t, err)
   542  	assert.Empty(t, data)
   543  
   544  	// Test Ranging on an Index
   545  
   546  	// Get "1" partition
   547  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   548  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   549  	}, dosa.All(), "", 200)
   550  	assert.NoError(t, err)
   551  	assert.Len(t, data, 10)
   552  	assert.Empty(t, token)
   553  
   554  	// Get the "2" partition, should be empty
   555  	data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   556  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(2))}},
   557  	}, dosa.All(), "", 200)
   558  	assert.NoError(t, err)
   559  	assert.Empty(t, data)
   560  	assert.Empty(t, token)
   561  }
   562  
   563  func TestConnector_TimeUUIDs(t *testing.T) {
   564  	sut := NewConnector()
   565  	const idcount = 10
   566  
   567  	// insert a bunch of values with V1 timestamps as clustering keys
   568  	for x := 0; x < idcount; x++ {
   569  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   570  			"f1": dosa.FieldValue("data"),
   571  			"c1": dosa.FieldValue(int64(1)),
   572  			"c6": dosa.FieldValue(int32(x)),
   573  			"c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))})
   574  		assert.NoError(t, err)
   575  	}
   576  
   577  	// read them back, they should be in reverse order
   578  	data, _, _ := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   579  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   580  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   581  	}, dosa.All(), "", 200)
   582  
   583  	// check that the order is backwards
   584  	for idx, row := range data {
   585  		assert.Equal(t, int32(idcount-idx-1), row["c6"])
   586  	}
   587  
   588  	// now mix in a few V4 UUIDs
   589  	for x := 0; x < idcount; x++ {
   590  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   591  			"f1": dosa.FieldValue("data"),
   592  			"c1": dosa.FieldValue(int64(1)),
   593  			"c6": dosa.FieldValue(int32(idcount + x)),
   594  			"c7": dosa.FieldValue(dosa.NewUUID())})
   595  		assert.NoError(t, err)
   596  	}
   597  
   598  	// the V4's should all be first, since V4 UUIDs sort > V1 UUIDs
   599  	data, _, _ = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   600  		"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   601  		"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   602  	}, dosa.All(), "", 200)
   603  	for _, row := range data[0:idcount] {
   604  		assert.True(t, row["c6"].(int32) >= idcount, row["c6"])
   605  	}
   606  
   607  }
   608  
   609  type ByUUID []dosa.UUID
   610  
   611  func (u ByUUID) Len() int           { return len(u) }
   612  func (u ByUUID) Swap(i, j int)      { u[i], u[j] = u[j], u[i] }
   613  func (u ByUUID) Less(i, j int) bool { return string(u[i]) > string(u[j]) }
   614  
   615  func BenchmarkConnector_CreateIfNotExists(b *testing.B) {
   616  	sut := NewConnector()
   617  	for x := 0; x < b.N; x++ {
   618  		id := dosa.NewUUID()
   619  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   620  			"f1": dosa.FieldValue("key"),
   621  			"c1": dosa.FieldValue(int64(1)),
   622  			"c7": dosa.FieldValue(id)})
   623  		assert.NoError(b, err)
   624  		if x%1000 == 0 {
   625  			sut.data = nil
   626  		}
   627  	}
   628  }
   629  
   630  func BenchmarkConnector_Read(b *testing.B) {
   631  	const idcount = 100
   632  	testUUIDs := make([]dosa.UUID, idcount)
   633  	for x := 0; x < idcount; x++ {
   634  		testUUIDs[x] = dosa.NewUUID()
   635  	}
   636  	sut := NewConnector()
   637  	for x := 0; x < idcount; x++ {
   638  		err := sut.CreateIfNotExists(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   639  			"f1": dosa.FieldValue("data"),
   640  			"c1": dosa.FieldValue(int64(1)),
   641  			"c7": dosa.FieldValue(testUUIDs[x])})
   642  		assert.NoError(b, err)
   643  	}
   644  
   645  	for x := 0; x < b.N; x++ {
   646  		_, err := sut.Read(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   647  			"f1": dosa.FieldValue("data"),
   648  			"c1": dosa.FieldValue(int64(1)),
   649  			"c7": dosa.FieldValue(testUUIDs[x%idcount])}, dosa.All())
   650  		assert.NoError(b, err)
   651  	}
   652  }
   653  
   654  func TestCompareType(t *testing.T) {
   655  	tuuid := dosa.NewUUID()
   656  	v1uuid := dosa.UUID(uuid.NewV1().String())
   657  	v1newer := dosa.UUID(uuid.NewV1().String())
   658  	tests := []struct {
   659  		t1, t2 dosa.FieldValue
   660  		result int8
   661  	}{
   662  		{dosa.FieldValue(int32(1)), dosa.FieldValue(int32(1)), 0},
   663  		{dosa.FieldValue(int64(1)), dosa.FieldValue(int64(1)), 0},
   664  		{dosa.FieldValue("test"), dosa.FieldValue("test"), 0},
   665  		{dosa.FieldValue(time.Time{}), dosa.FieldValue(time.Time{}), 0},
   666  		{dosa.FieldValue(tuuid), dosa.FieldValue(tuuid), 0},
   667  		{dosa.FieldValue(v1uuid), dosa.FieldValue(v1uuid), 0},
   668  		{dosa.FieldValue(false), dosa.FieldValue(false), 0},
   669  		{dosa.FieldValue([]byte{1}), dosa.FieldValue([]byte{1}), 0},
   670  		{dosa.FieldValue(1.0), dosa.FieldValue(1.0), 0},
   671  
   672  		{dosa.FieldValue(int32(1)), dosa.FieldValue(int32(2)), -1},
   673  		{dosa.FieldValue(int64(1)), dosa.FieldValue(int64(2)), -1},
   674  		{dosa.FieldValue("test"), dosa.FieldValue("test2"), -1},
   675  		{dosa.FieldValue(time.Time{}), dosa.FieldValue(time.Time{}.Add(time.Duration(1))), -1},
   676  		{dosa.FieldValue(v1uuid), dosa.FieldValue(tuuid), -1},
   677  		{dosa.FieldValue(v1uuid), dosa.FieldValue(v1newer), -1},
   678  		{dosa.FieldValue(false), dosa.FieldValue(true), -1},
   679  		{dosa.FieldValue([]byte{1}), dosa.FieldValue([]byte{2}), -1},
   680  		{dosa.FieldValue(0.9), dosa.FieldValue(1.0), -1},
   681  
   682  		{dosa.FieldValue(int32(2)), dosa.FieldValue(int32(1)), 1},
   683  		{dosa.FieldValue(int64(2)), dosa.FieldValue(int64(1)), 1},
   684  		{dosa.FieldValue("test2"), dosa.FieldValue("test"), 1},
   685  		{dosa.FieldValue(time.Time{}.Add(time.Duration(1))), dosa.FieldValue(time.Time{}), 1},
   686  		{dosa.FieldValue(tuuid), dosa.FieldValue(v1uuid), 1},
   687  		{dosa.FieldValue(v1newer), dosa.FieldValue(v1uuid), 1},
   688  		{dosa.FieldValue(true), dosa.FieldValue(false), 1},
   689  		{dosa.FieldValue([]byte{2}), dosa.FieldValue([]byte{1}), 1},
   690  		{dosa.FieldValue(1.1), dosa.FieldValue(1.0), 1},
   691  	}
   692  	for _, test := range tests {
   693  		assert.Equal(t, test.result, compareType(test.t1, test.t2))
   694  	}
   695  
   696  	assert.Panics(t, func() { compareType(t, t) })
   697  }
   698  
   699  func TestConnector_Scan(t *testing.T) {
   700  	sut := NewConnector()
   701  	const idcount = 10
   702  
   703  	testUUIDs := make([]dosa.UUID, idcount)
   704  	for x := 0; x < idcount; x++ {
   705  		testUUIDs[x] = dosa.NewUUID()
   706  	}
   707  	// scan with nothing there yet
   708  	_, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100)
   709  	assert.NoError(t, err)
   710  	assert.Empty(t, token)
   711  
   712  	// first, insert some random UUID values into two partition keys
   713  	for x := 0; x < idcount; x++ {
   714  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   715  			"f1": dosa.FieldValue("data" + string(x%2)),
   716  			"c1": dosa.FieldValue(int64(1)),
   717  			"c7": dosa.FieldValue(testUUIDs[x])})
   718  		assert.NoError(t, err)
   719  	}
   720  
   721  	data, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100)
   722  	assert.NoError(t, err)
   723  	assert.Len(t, data, idcount)
   724  	assert.Empty(t, token)
   725  
   726  	// there's an odd edge case when you delete everything, so do that, then call scan
   727  	for x := 0; x < idcount; x++ {
   728  		err := sut.Remove(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   729  			"f1": dosa.FieldValue("data" + string(x%2)),
   730  			"c1": dosa.FieldValue(int64(1)),
   731  			"c7": dosa.FieldValue(testUUIDs[x])})
   732  		assert.NoError(t, err)
   733  	}
   734  	data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 100)
   735  	assert.NoError(t, err)
   736  	assert.Empty(t, data)
   737  	assert.Empty(t, token)
   738  }
   739  
   740  func TestConnector_ScanWithToken(t *testing.T) {
   741  	sut := NewConnector()
   742  	const idcount = 100
   743  	createTestData(t, sut, func(id int) string {
   744  		return "data" + string(id%3)
   745  	}, idcount)
   746  	var token string
   747  	var err error
   748  	var data []map[string]dosa.FieldValue
   749  	for x := 0; x < idcount; x++ {
   750  		data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1)
   751  		assert.NoError(t, err)
   752  		assert.Len(t, data, 1)
   753  		if x < idcount-1 {
   754  			assert.NotEmpty(t, token)
   755  		} else {
   756  			assert.Empty(t, token)
   757  		}
   758  	}
   759  	// now walk through again, but delete the item returned
   760  	// (note: we don't have to reset token, because it should be empty now)
   761  	for x := 0; x < idcount; x++ {
   762  		data, token, err = sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1)
   763  		assert.NoError(t, err)
   764  		assert.Len(t, data, 1)
   765  		if x < idcount-1 {
   766  			assert.NotEmpty(t, token)
   767  		} else {
   768  			assert.Empty(t, token)
   769  		}
   770  		err = sut.Remove(context.TODO(), clusteredEi, data[0])
   771  		assert.NoError(t, err)
   772  	}
   773  }
   774  
   775  func TestConnector_ScanWithTokenFromWrongTable(t *testing.T) {
   776  	sut := NewConnector()
   777  	const idcount = 100
   778  	createTestData(t, sut, func(id int) string {
   779  		return "data" + string(id%3)
   780  	}, idcount)
   781  	err := sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{
   782  		"p1": dosa.FieldValue("test"),
   783  	})
   784  	assert.NoError(t, err)
   785  
   786  	// get a token from one table
   787  	_, token, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), "", 1)
   788  	assert.NoError(t, err)
   789  	assert.NotEmpty(t, token)
   790  
   791  	// now use it for another scan (oops)
   792  	_, _, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1)
   793  	assert.Error(t, err)
   794  	assert.Contains(t, err.Error(), "Invalid token")
   795  	assert.Contains(t, err.Error(), "Missing value")
   796  }
   797  
   798  func TestConnector_ScanWithTokenNoClustering(t *testing.T) {
   799  	sut := NewConnector()
   800  
   801  	// add some data
   802  	const idcount = 100
   803  	for x := 0; x < idcount; x++ {
   804  		sut.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{
   805  			"p1": dosa.FieldValue("data" + string(x)),
   806  		})
   807  	}
   808  	var token string
   809  	var err error
   810  	var data []map[string]dosa.FieldValue
   811  	for x := 0; x < idcount; x++ {
   812  		data, token, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1)
   813  		assert.NoError(t, err)
   814  		assert.Len(t, data, 1)
   815  		if x < idcount-1 {
   816  			assert.NotEmpty(t, token)
   817  		} else {
   818  			assert.Empty(t, token)
   819  		}
   820  	}
   821  	// and again, this time deleting as we go
   822  	for x := 0; x < idcount; x++ {
   823  		data, token, err = sut.Scan(context.TODO(), testEi, dosa.All(), token, 1)
   824  		assert.NoError(t, err)
   825  		assert.Len(t, data, 1)
   826  		if x < idcount-1 {
   827  			assert.NotEmpty(t, token)
   828  		} else {
   829  			assert.Empty(t, token)
   830  		}
   831  		err = sut.Remove(context.TODO(), testEi, data[0])
   832  		assert.NoError(t, err)
   833  	}
   834  
   835  }
   836  
   837  func TestConstruction(t *testing.T) {
   838  	c, err := dosa.GetConnector("memory", nil)
   839  	assert.NoError(t, err)
   840  	assert.IsType(t, NewConnector(), c)
   841  
   842  	v, err := c.CheckSchema(context.TODO(), "dummy", "dummy", nil)
   843  	assert.Equal(t, int32(1), v)
   844  	assert.NoError(t, err)
   845  }
   846  
   847  func TestPanics(t *testing.T) {
   848  	assert.Panics(t, func() {
   849  		passCol(dosa.FieldValue(int64(1)), &dosa.Condition{Op: 0, Value: dosa.FieldValue(int64(1))})
   850  	})
   851  }
   852  
   853  func TestRangePager(t *testing.T) {
   854  	sut := NewConnector()
   855  	idcount := 5
   856  	// create test data in one partition "data"
   857  	createTestData(t, sut, func(_ int) string { return "data" }, idcount)
   858  	var token string
   859  	var err error
   860  	var data []map[string]dosa.FieldValue
   861  	for x := 0; x < idcount; x++ {
   862  		data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   863  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   864  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   865  		}, dosa.All(), token, 1)
   866  		assert.NoError(t, err)
   867  		assert.Len(t, data, 1)
   868  		if x < idcount-1 {
   869  			assert.NotEmpty(t, token)
   870  		} else {
   871  			assert.Empty(t, token)
   872  		}
   873  	}
   874  	// now walk through again, but delete the item returned
   875  	// (note: we don't have to reset token, because it should be empty now)
   876  	for x := 0; x < idcount; x++ {
   877  		data, token, err = sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   878  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   879  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   880  		}, dosa.All(), token, 1)
   881  		assert.NoError(t, err)
   882  		assert.Len(t, data, 1)
   883  		if x < idcount-1 {
   884  			assert.NotEmpty(t, token)
   885  		} else {
   886  			assert.Empty(t, token)
   887  		}
   888  		err = sut.Remove(context.TODO(), clusteredEi, data[0])
   889  		assert.NoError(t, err)
   890  	}
   891  }
   892  func TestInvalidToken(t *testing.T) {
   893  	sut := NewConnector()
   894  
   895  	// we don't use the token if there's no data that matches, so lets
   896  	// create one row
   897  	createTestData(t, sut, func(id int) string {
   898  		return "data"
   899  	}, 1)
   900  
   901  	token := "this is not a token and not a hot dog"
   902  	t.Run("testInvalidTokenRange", func(t *testing.T) {
   903  		_, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   904  			"f1": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   905  			"c1": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   906  		}, dosa.All(), token, 1)
   907  		assert.Error(t, err)
   908  		assert.Contains(t, err.Error(), "Invalid token")
   909  	})
   910  	t.Run("testInvalidTokenScan", func(t *testing.T) {
   911  		_, _, err := sut.Scan(context.TODO(), clusteredEi, dosa.All(), token, 1)
   912  		assert.Error(t, err)
   913  		assert.Contains(t, err.Error(), "Invalid token")
   914  	})
   915  }
   916  
   917  // TestEncoderPanic covers the one panic() in makeToken
   918  // This panic should only happen if someone passes a dosa.FieldValue that points to
   919  // something we don't know how to handle
   920  func TestEncoderPanic(t *testing.T) {
   921  	assert.Panics(t, func() {
   922  		makeToken(map[string]dosa.FieldValue{
   923  			"oops": func() {},
   924  		})
   925  	})
   926  }
   927  
   928  func TestConnector_RangeWithBadCriteria(t *testing.T) {
   929  	sut := NewConnector()
   930  	// we don't look at the criteria unless there is at least one row
   931  	createTestData(t, sut, func(id int) string {
   932  		return "data"
   933  	}, 1)
   934  
   935  	_, _, err := sut.Range(context.TODO(), clusteredEi, map[string][]*dosa.Condition{
   936  		"c2": {{Op: dosa.Eq, Value: dosa.FieldValue("data")}},
   937  		"c3": {{Op: dosa.Eq, Value: dosa.FieldValue(int64(1))}},
   938  	}, dosa.All(), "", 1)
   939  	assert.Error(t, err)
   940  
   941  }
   942  
   943  // createTestData populates some test data. The keyGenFunc can either return a constant,
   944  // which gives you a single partition of data, or some function of the current offset, which
   945  // will scatter the data across different partition keys
   946  func createTestData(t *testing.T, sut *Connector, keyGenFunc func(int) string, idcount int) {
   947  	// insert a bunch of values with V1 timestamps as clustering keys
   948  	for x := 0; x < idcount; x++ {
   949  		err := sut.Upsert(context.TODO(), clusteredEi, map[string]dosa.FieldValue{
   950  			"f1": dosa.FieldValue(keyGenFunc(x)),
   951  			"c1": dosa.FieldValue(int64(1)),
   952  			"c6": dosa.FieldValue(int32(x)),
   953  			"c7": dosa.FieldValue(dosa.UUID(uuid.NewV1().String()))})
   954  		assert.NoError(t, err)
   955  	}
   956  }