github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/memory/memory.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  	"bytes"
    25  	"context"
    26  	"encoding/gob"
    27  	"sort"
    28  	"sync"
    29  	"time"
    30  
    31  	"encoding/binary"
    32  
    33  	"encoding/base64"
    34  
    35  	"github.com/pkg/errors"
    36  	"github.com/satori/go.uuid"
    37  	"github.com/uber-go/dosa"
    38  	"github.com/uber-go/dosa/connectors/base"
    39  )
    40  
    41  // Connector is an in-memory connector.
    42  // The in-memory connector stores its data like this:
    43  // map[string]map[string][]map[string]dosa.FieldValue
    44  //
    45  // the first 'string' is the table name (entity name)
    46  // the second 'string' is the partition key, encoded using encoding/gob to guarantee uniqueness
    47  // within each 'partition' you have a list of rows ([]map[string]dosa.FieldValue)
    48  // these rows are kept ordered so that reads are lightning fast and searches are quick too
    49  // the row itself is a map of field name to value (map[string]dosaFieldValue])
    50  //
    51  // A read-write mutex lock is used to control concurrency, making reads work in parallel but
    52  // writes are not. There is no attempt to improve the concurrency of the read or write path by
    53  // adding more granular locks.
    54  type Connector struct {
    55  	base.Connector
    56  	data map[string]map[string][]map[string]dosa.FieldValue
    57  	lock sync.RWMutex
    58  }
    59  
    60  // partitionRange represents one section of a partition.
    61  type partitionRange struct {
    62  	entityRef    map[string][]map[string]dosa.FieldValue
    63  	partitionKey string
    64  	start        int
    65  	end          int
    66  }
    67  
    68  // remove deletes the values referenced by the partitionRange. Since this function modifies
    69  // the data stored in the in-memory connector, a write lock must be held when calling
    70  // this function.
    71  //
    72  // Note this function can't be called more than once. Calling it more than once will cause a panic.
    73  func (pr *partitionRange) remove() {
    74  	partitionRef := pr.entityRef[pr.partitionKey]
    75  	pr.entityRef[pr.partitionKey] = append(partitionRef[:pr.start], partitionRef[pr.end+1:]...)
    76  	pr.entityRef = nil
    77  	pr.partitionKey = ""
    78  	pr.start = 0
    79  	pr.end = 0
    80  }
    81  
    82  // values returns all the values in the partition range
    83  func (pr *partitionRange) values() []map[string]dosa.FieldValue {
    84  	return pr.entityRef[pr.partitionKey][pr.start : pr.end+1]
    85  }
    86  
    87  // partitionKeyBuilder extracts the partition key components from the map and encodes them,
    88  // generating a unique string. It uses the encoding/gob method to make a byte array as the
    89  // key, and returns this as a string
    90  func partitionKeyBuilder(pk *dosa.PrimaryKey, values map[string]dosa.FieldValue) (string, error) {
    91  	encodedKey := bytes.Buffer{}
    92  	encoder := gob.NewEncoder(&encodedKey)
    93  	for _, k := range pk.PartitionKeys {
    94  		if v, ok := values[k]; ok {
    95  			_ = encoder.Encode(v)
    96  		} else {
    97  			return "", errors.Errorf("Missing value for partition key %q", k)
    98  		}
    99  	}
   100  	return string(encodedKey.Bytes()), nil
   101  }
   102  
   103  // findInsertionPoint locates the place within a partition where the data belongs.
   104  // It inspects the clustering key values found in the insertMe value and figures out
   105  // where they go in the data slice. It doesn't change anything, but it does let you
   106  // know if it found an exact match or if it's just not there. When it's not there,
   107  // it indicates where it is supposed to get inserted
   108  func findInsertionPoint(pk *dosa.PrimaryKey, data []map[string]dosa.FieldValue, insertMe map[string]dosa.FieldValue) (found bool, idx int) {
   109  	found = false
   110  	idx = sort.Search(len(data), func(offset int) bool {
   111  		cmp := compareRows(pk, data[offset], insertMe)
   112  		if cmp == 0 {
   113  			found = true
   114  		}
   115  		return cmp >= 0
   116  	})
   117  	return
   118  }
   119  
   120  // compareRows compares two maps of row data based on clustering keys. It handles ascending/descending
   121  // based on the passed-in schema
   122  func compareRows(pk *dosa.PrimaryKey, v1 map[string]dosa.FieldValue, v2 map[string]dosa.FieldValue) (cmp int8) {
   123  	keys := pk.ClusteringKeys
   124  	for _, key := range keys {
   125  		d1 := v1[key.Name]
   126  		d2 := v2[key.Name]
   127  		cmp = compareType(d1, d2)
   128  		if key.Descending {
   129  			cmp = -cmp
   130  		}
   131  		if cmp != 0 {
   132  			return cmp
   133  		}
   134  	}
   135  	return cmp
   136  }
   137  
   138  // This function returns the time bits from a UUID
   139  // You would have to scale this to nanos to make a
   140  // time.Time but we don't generally need that for
   141  // comparisons. See RFC 4122 for these bit offsets
   142  func timeFromUUID(u uuid.UUID) int64 {
   143  	low := int64(binary.BigEndian.Uint32(u[0:4]))
   144  	mid := int64(binary.BigEndian.Uint16(u[4:6]))
   145  	hi := int64((binary.BigEndian.Uint16(u[6:8]) & 0x0fff))
   146  	return low + (mid << 32) + (hi << 48)
   147  }
   148  
   149  // compareType compares a single DOSA field based on the type. This code assumes the types of each
   150  // of the columns are the same, or it will panic
   151  func compareType(d1 dosa.FieldValue, d2 dosa.FieldValue) int8 {
   152  	switch d1 := d1.(type) {
   153  	case dosa.UUID:
   154  		u1 := uuid.FromStringOrNil(string(d1))
   155  		u2 := uuid.FromStringOrNil(string(d2.(dosa.UUID)))
   156  		if u1.Version() != u2.Version() {
   157  			if u1.Version() < u2.Version() {
   158  				return -1
   159  			}
   160  			return 1
   161  		}
   162  		if u1.Version() == 1 {
   163  			// compare time UUIDs
   164  			t1 := timeFromUUID(u1)
   165  			t2 := timeFromUUID(u2)
   166  			if t1 == t2 {
   167  				return 0
   168  			}
   169  			if t1 < t2 {
   170  				return -1
   171  			}
   172  			return 1
   173  		}
   174  
   175  		// version
   176  		if string(d1) == string(d2.(dosa.UUID)) {
   177  			return 0
   178  		}
   179  		if string(d1) < string(d2.(dosa.UUID)) {
   180  			return -1
   181  		}
   182  		return 1
   183  	case string:
   184  		if d1 == d2.(string) {
   185  			return 0
   186  		}
   187  		if d1 < d2.(string) {
   188  			return -1
   189  		}
   190  		return 1
   191  	case int64:
   192  		if d1 == d2.(int64) {
   193  			return 0
   194  		}
   195  		if d1 < d2.(int64) {
   196  			return -1
   197  		}
   198  		return 1
   199  	case int32:
   200  		if d1 == d2.(int32) {
   201  			return 0
   202  		}
   203  		if d1 < d2.(int32) {
   204  			return -1
   205  		}
   206  		return 1
   207  	case float64:
   208  		if d1 == d2.(float64) {
   209  			return 0
   210  		}
   211  		if d1 < d2.(float64) {
   212  			return -1
   213  		}
   214  		return 1
   215  	case []byte:
   216  		c := bytes.Compare(d1, d2.([]byte))
   217  		if c == 0 {
   218  			return 0
   219  		}
   220  		if c < 0 {
   221  			return -1
   222  		}
   223  		return 1
   224  	case time.Time:
   225  		if d1.Equal(d2.(time.Time)) {
   226  			return 0
   227  		}
   228  		if d1.Before(d2.(time.Time)) {
   229  			return -1
   230  		}
   231  		return 1
   232  	case bool:
   233  		if d1 == d2.(bool) {
   234  			return 0
   235  		}
   236  		if d1 == false {
   237  			return -1
   238  		}
   239  		return 1
   240  	}
   241  	panic(d1)
   242  }
   243  
   244  // CreateIfNotExists inserts a row if it isn't already there. The basic flow is:
   245  // Find the partition, if it's not there, then create it and insert the row there
   246  // If the partition is there, and there's data in it, and there's no clustering key, then fail
   247  // Otherwise, search the partition for the exact same clustering keys. If there, fail
   248  // if not, then insert it at the right spot (sort.Search does most of the heavy lifting here)
   249  func (c *Connector) CreateIfNotExists(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   250  	c.lock.Lock()
   251  	defer c.lock.Unlock()
   252  	err := c.mergedInsert(ei.Def.Name, ei.Def.Key, values, func(into map[string]dosa.FieldValue, from map[string]dosa.FieldValue) error {
   253  		return &dosa.ErrAlreadyExists{}
   254  	})
   255  	if err != nil {
   256  		return err
   257  	}
   258  	for iName, iDef := range ei.Def.Indexes {
   259  		// this error must be ignored, so we skip indexes when the value
   260  		// for one of the index fields is not specified
   261  		_ = c.mergedInsert(iName, ei.Def.UniqueKey(iDef.Key), values, overwriteValuesFunc)
   262  	}
   263  	return nil
   264  }
   265  
   266  // Read searches for a row. First, it finds the partition, then it searches in the partition for
   267  // the data, and returns it when it finds it. Again, sort.Search does most of the heavy lifting
   268  // within a partition
   269  func (c *Connector) Read(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue, minimumFields []string) (map[string]dosa.FieldValue, error) {
   270  	c.lock.RLock()
   271  	defer c.lock.RUnlock()
   272  	entityRef := c.data[ei.Def.Name]
   273  	encodedPartitionKey, err := partitionKeyBuilder(ei.Def.Key, values)
   274  	if err != nil {
   275  		return nil, errors.Wrapf(err, "Cannot build partition key for entity %q", ei.Def.Name)
   276  	}
   277  	if c.data[ei.Def.Name] == nil {
   278  		return nil, &dosa.ErrNotFound{}
   279  	}
   280  	partitionRef := entityRef[encodedPartitionKey]
   281  	// no data in this partition? easy out!
   282  	if len(partitionRef) == 0 {
   283  		return nil, &dosa.ErrNotFound{}
   284  	}
   285  
   286  	if len(ei.Def.Key.ClusteringKeySet()) == 0 {
   287  		return partitionRef[0], nil
   288  	}
   289  	// clustering key, search for the value in the set
   290  	found, inx := findInsertionPoint(ei.Def.Key, partitionRef, values)
   291  	if !found {
   292  		return nil, &dosa.ErrNotFound{}
   293  	}
   294  	return partitionRef[inx], nil
   295  }
   296  
   297  func overwriteValuesFunc(into map[string]dosa.FieldValue, from map[string]dosa.FieldValue) error {
   298  	for k, v := range from {
   299  		into[k] = v
   300  	}
   301  	return nil
   302  }
   303  
   304  // Upsert works a lot like CreateIfNotExists but merges the data when it finds an existing row
   305  func (c *Connector) Upsert(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   306  	c.lock.Lock()
   307  	defer c.lock.Unlock()
   308  	if err := c.mergedInsert(ei.Def.Name, ei.Def.Key, values, overwriteValuesFunc); err != nil {
   309  		return err
   310  	}
   311  	for iName, iDef := range ei.Def.Indexes {
   312  		_ = c.mergedInsert(iName, ei.Def.UniqueKey(iDef.Key), values, overwriteValuesFunc)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  func (c *Connector) mergedInsert(name string,
   319  	pk *dosa.PrimaryKey,
   320  	values map[string]dosa.FieldValue,
   321  	mergeFunc func(map[string]dosa.FieldValue, map[string]dosa.FieldValue) error) error {
   322  
   323  	if c.data[name] == nil {
   324  		c.data[name] = make(map[string][]map[string]dosa.FieldValue)
   325  	}
   326  	entityRef := c.data[name]
   327  	encodedPartitionKey, err := partitionKeyBuilder(pk, values)
   328  	if err != nil {
   329  		return errors.Wrapf(err, "Cannot build partition key for %q", name)
   330  	}
   331  	if entityRef[encodedPartitionKey] == nil {
   332  		entityRef[encodedPartitionKey] = make([]map[string]dosa.FieldValue, 0, 1)
   333  	}
   334  	partitionRef := entityRef[encodedPartitionKey]
   335  	// no data in this partition? easy out!
   336  	if len(partitionRef) == 0 {
   337  		entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey], values)
   338  		return nil
   339  	}
   340  
   341  	if len(pk.ClusteringKeySet()) == 0 {
   342  		// no clustering key, so the row must already exist, merge it
   343  		return mergeFunc(partitionRef[0], values)
   344  	}
   345  	// there is a clustering key, find the insertion point (binary search would be fastest)
   346  	found, offset := findInsertionPoint(pk, partitionRef, values)
   347  	if found {
   348  		return mergeFunc(partitionRef[offset], values)
   349  	}
   350  	// perform slice magic to insert value at given offset
   351  	l := len(entityRef[encodedPartitionKey])                                                                     // get length
   352  	entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey], entityRef[encodedPartitionKey][l-1]) // copy last element
   353  	// scoot over remaining elements
   354  	copy(entityRef[encodedPartitionKey][offset+1:], entityRef[encodedPartitionKey][offset:])
   355  	// and plunk value into appropriate location
   356  	entityRef[encodedPartitionKey][offset] = values
   357  	return nil
   358  }
   359  
   360  // Remove deletes a single row
   361  // There's no way to return an error from this method
   362  func (c *Connector) Remove(_ context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   363  	c.lock.Lock()
   364  	defer c.lock.Unlock()
   365  	if c.data[ei.Def.Name] == nil {
   366  		return nil
   367  	}
   368  	for iName, iDef := range ei.Def.Indexes {
   369  		c.removeItem(iName, ei.Def.UniqueKey(iDef.Key), values)
   370  	}
   371  	c.removeItem(ei.Def.Name, ei.Def.Key, values)
   372  	return nil
   373  }
   374  
   375  func (c *Connector) removeItem(name string, key *dosa.PrimaryKey, values map[string]dosa.FieldValue) {
   376  	entityRef := c.data[name]
   377  	encodedPartitionKey, err := partitionKeyBuilder(key, values)
   378  	if err != nil || entityRef[encodedPartitionKey] == nil {
   379  		return
   380  	}
   381  	partitionRef := entityRef[encodedPartitionKey]
   382  	// no data in this partition? easy out!
   383  	if len(partitionRef) == 0 {
   384  		return
   385  	}
   386  
   387  	// no clustering keys? Simple, delete this
   388  	if len(key.ClusteringKeySet()) == 0 {
   389  		// NOT delete(entityRef, encodedPartitionKey)
   390  		// Unfortunately, Scan relies on the fact that these are not completely deleted
   391  		entityRef[encodedPartitionKey] = nil
   392  		return
   393  	}
   394  	found, offset := findInsertionPoint(key, partitionRef, values)
   395  	if found {
   396  		entityRef[encodedPartitionKey] = append(entityRef[encodedPartitionKey][:offset], entityRef[encodedPartitionKey][offset+1:]...)
   397  	}
   398  	return
   399  }
   400  
   401  // RemoveRange removes all of the elements in the range specified by the entity info and the column conditions.
   402  func (c *Connector) RemoveRange(_ context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) error {
   403  	c.lock.Lock()
   404  	defer c.lock.Unlock()
   405  
   406  	partitionRange, err := c.findRange(ei, columnConditions)
   407  	if err != nil {
   408  		return err
   409  	}
   410  	// TODO: this should have been from a primary key lookup, not an index lookup
   411  	if partitionRange != nil {
   412  		for iName, iDef := range ei.Def.Indexes {
   413  			for _, vals := range partitionRange.values() {
   414  				c.removeItem(iName, ei.Def.UniqueKey(iDef.Key), vals)
   415  			}
   416  		}
   417  		partitionRange.remove()
   418  	}
   419  
   420  	return nil
   421  }
   422  
   423  // Range returns a slice of data from the datastore
   424  func (c *Connector) Range(_ context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) {
   425  	c.lock.RLock()
   426  	defer c.lock.RUnlock()
   427  
   428  	partitionRange, err := c.findRange(ei, columnConditions)
   429  	if err != nil {
   430  		return nil, "", errors.Wrap(err, "Invalid range conditions")
   431  	}
   432  	if partitionRange == nil {
   433  		return []map[string]dosa.FieldValue{}, "", nil
   434  	}
   435  
   436  	if token != "" {
   437  		// if we have a token, use it to determine the offset to start from
   438  		values, err := decodeToken(token)
   439  		if err != nil {
   440  			return nil, "", errors.Wrapf(err, "Invalid token %q", token)
   441  		}
   442  		found, offset := findInsertionPoint(ei.Def.Key, partitionRange.values(), values)
   443  		if found {
   444  			partitionRange.start += offset + 1
   445  		} else {
   446  			partitionRange.start += offset
   447  		}
   448  	}
   449  	slice := partitionRange.values()
   450  	token = ""
   451  	if len(slice) > limit {
   452  		token = makeToken(slice[limit-1])
   453  		slice = slice[:limit]
   454  	}
   455  	return slice, token, nil
   456  }
   457  
   458  func makeToken(v map[string]dosa.FieldValue) string {
   459  	encodedKey := bytes.Buffer{}
   460  	encoder := gob.NewEncoder(&encodedKey)
   461  	gob.Register(dosa.UUID(""))
   462  	err := encoder.Encode(v)
   463  	if err != nil {
   464  		// this should really be impossible, unless someone forgot to
   465  		// register some newly supported type with the encoder
   466  		panic(err)
   467  	}
   468  	return base64.StdEncoding.EncodeToString([]byte(encodedKey.String()))
   469  }
   470  
   471  func decodeToken(token string) (values map[string]dosa.FieldValue, err error) {
   472  	gobData, err := base64.StdEncoding.DecodeString(token)
   473  	if err != nil {
   474  		return nil, err
   475  	}
   476  	gobReader := bytes.NewBuffer(gobData)
   477  	gob.Register(dosa.UUID(""))
   478  	decoder := gob.NewDecoder(gobReader)
   479  	err = decoder.Decode(&values)
   480  	return values, err
   481  }
   482  
   483  // findRange finds the partitionRange specified by the given entity info and column conditions.
   484  // In the case that no entities are found an empty partitionRange with a nil partition field will be returned.
   485  //
   486  // Note that this function reads from the connector's data map. Any calling functions should hold
   487  // at least a read lock on the map.
   488  func (c *Connector) findRange(ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) (*partitionRange, error) {
   489  	// no data at all, fine
   490  	if c.data[ei.Def.Name] == nil {
   491  		return nil, nil
   492  	}
   493  
   494  	// find the equals conditions on each of the partition keys
   495  	values := make(map[string]dosa.FieldValue)
   496  
   497  	// figure out which "table" or "index" to use based on the supplied conditions
   498  	name, key, err := ei.IndexFromConditions(columnConditions)
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  
   503  	for _, pk := range key.PartitionKeys {
   504  		values[pk] = columnConditions[pk][0].Value
   505  	}
   506  
   507  	entityRef := c.data[name]
   508  	// an error is impossible here, since the partition keys must be set from IndexFromConditions
   509  	encodedPartitionKey, _ := partitionKeyBuilder(key, values)
   510  	partitionRef := entityRef[encodedPartitionKey]
   511  	// no data in this partition? easy out!
   512  	if len(partitionRef) == 0 {
   513  		return nil, nil
   514  	}
   515  	// hunt through the partitionRef and return values that match search criteria
   516  	// TODO: This can be done much faster using a binary search
   517  	startinx, endinx := 0, len(partitionRef)-1
   518  	for startinx < len(partitionRef) && !matchesClusteringConditions(ei, columnConditions, partitionRef[startinx]) {
   519  		startinx++
   520  	}
   521  	for endinx >= startinx && !matchesClusteringConditions(ei, columnConditions, partitionRef[endinx]) {
   522  		endinx--
   523  
   524  	}
   525  	if endinx < startinx {
   526  		return nil, nil
   527  	}
   528  
   529  	return &partitionRange{
   530  		entityRef:    entityRef,
   531  		partitionKey: encodedPartitionKey,
   532  		start:        startinx,
   533  		end:          endinx,
   534  	}, nil
   535  }
   536  
   537  // matchesClusteringConditions checks if a data row matches the conditions in the columnConditions that apply to
   538  // clustering columns. If a condition does NOT match, it returns false, otherwise true
   539  // This function is pretty fast if there are no conditions on the clustering columns
   540  func matchesClusteringConditions(ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, data map[string]dosa.FieldValue) bool {
   541  	for _, col := range ei.Def.Key.ClusteringKeys {
   542  		if conds, ok := columnConditions[col.Name]; ok {
   543  			// conditions exist on this clustering key
   544  			for _, cond := range conds {
   545  				if !passCol(data[col.Name], cond) {
   546  					return false
   547  				}
   548  			}
   549  		}
   550  	}
   551  	return true
   552  }
   553  
   554  // passCol checks if a column passes a specific condition
   555  func passCol(data dosa.FieldValue, cond *dosa.Condition) bool {
   556  	cmp := compareType(data, cond.Value)
   557  	switch cond.Op {
   558  	case dosa.Eq:
   559  		return cmp == 0
   560  	case dosa.Gt:
   561  		return cmp > 0
   562  	case dosa.GtOrEq:
   563  		return cmp >= 0
   564  	case dosa.Lt:
   565  		return cmp < 0
   566  	case dosa.LtOrEq:
   567  		return cmp <= 0
   568  	}
   569  	panic("invalid operator " + cond.Op.String())
   570  }
   571  
   572  // Scan returns all the rows
   573  func (c *Connector) Scan(_ context.Context, ei *dosa.EntityInfo, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) {
   574  	c.lock.RLock()
   575  	defer c.lock.RUnlock()
   576  	if c.data[ei.Def.Name] == nil {
   577  		return []map[string]dosa.FieldValue{}, "", nil
   578  
   579  	}
   580  	entityRef := c.data[ei.Def.Name]
   581  	allTheThings := make([]map[string]dosa.FieldValue, 0)
   582  
   583  	// in order for Scan to be deterministic and continuable, we have
   584  	// to sort the primary key references
   585  	keys := make([]string, 0, len(entityRef))
   586  	for key := range entityRef {
   587  		keys = append(keys, key)
   588  	}
   589  	sort.Strings(keys)
   590  
   591  	// if there was a token, decode it so we can determine the starting
   592  	// partition key
   593  	startPartKey, start, err := getStartingPoint(ei, token)
   594  	if err != nil {
   595  		return nil, "", errors.Wrapf(err, "Invalid token %s", token)
   596  	}
   597  
   598  	for _, key := range keys {
   599  		if startPartKey != "" {
   600  			// had a token, so we need to either partially skip or fully skip
   601  			// depending on whether we found the token's partition key yet
   602  			if key == startPartKey {
   603  				// we reached the starting partition key, so stop skipping
   604  				// future values, and add a portion of this one to the set
   605  				startPartKey = ""
   606  				found, offset := findInsertionPoint(ei.Def.Key, entityRef[key], start)
   607  				if found {
   608  					offset++
   609  				}
   610  				allTheThings = append(allTheThings, entityRef[key][offset:]...)
   611  			} // else keep looking for this partition key
   612  			continue
   613  		}
   614  		allTheThings = append(allTheThings, entityRef[key]...)
   615  	}
   616  	if len(allTheThings) == 0 {
   617  		return []map[string]dosa.FieldValue{}, "", nil
   618  	}
   619  	// see if we need a token to return
   620  	token = ""
   621  	if len(allTheThings) > limit {
   622  		token = makeToken(allTheThings[limit-1])
   623  		allTheThings = allTheThings[:limit]
   624  	}
   625  	return allTheThings, token, nil
   626  }
   627  
   628  // getStartingPoint determines the partition key of the starting point to resume a scan
   629  // when a token is provided
   630  func getStartingPoint(ei *dosa.EntityInfo, token string) (start string, startPartKey map[string]dosa.FieldValue, err error) {
   631  	if token == "" {
   632  		return "", map[string]dosa.FieldValue{}, nil
   633  	}
   634  	startPartKey, err = decodeToken(token)
   635  	if err != nil {
   636  		return "", map[string]dosa.FieldValue{}, errors.Wrapf(err, "Invalid token %q", token)
   637  	}
   638  	start, err = partitionKeyBuilder(ei.Def.Key, startPartKey)
   639  	if err != nil {
   640  		return "", map[string]dosa.FieldValue{}, errors.Wrapf(err, "Can't build partition key for %q", ei.Def.Name)
   641  	}
   642  	return start, startPartKey, nil
   643  }
   644  
   645  // CheckSchema is just a stub; there is no schema management for the in memory connector
   646  // since creating a new one leaves you with no data!
   647  func (c *Connector) CheckSchema(ctx context.Context, scope, namePrefix string, ed []*dosa.EntityDefinition) (int32, error) {
   648  	return 1, nil
   649  }
   650  
   651  // Shutdown deletes all the data
   652  func (c *Connector) Shutdown() error {
   653  	c.lock.Lock()
   654  	defer c.lock.Unlock()
   655  	c.data = nil
   656  	return nil
   657  }
   658  
   659  // NewConnector creates a new in-memory connector
   660  func NewConnector() *Connector {
   661  	c := Connector{}
   662  	c.data = make(map[string]map[string][]map[string]dosa.FieldValue)
   663  	return &c
   664  }
   665  
   666  func init() {
   667  	dosa.RegisterConnector("memory", func(dosa.CreationArgs) (dosa.Connector, error) {
   668  		return NewConnector(), nil
   669  	})
   670  }