diff --git a/state/queue_test.go b/state/queue_test.go index df367e5bb..d6212be90 100644 --- a/state/queue_test.go +++ b/state/queue_test.go @@ -9,6 +9,11 @@ import ( func TestQueue(t *testing.T) { assert := assert.New(t) + lots := make([][]byte, 500) + for i := range lots { + lots[i] = []byte{1, 8, 7} + } + cases := []struct { pushes [][]byte pops [][]byte @@ -28,6 +33,8 @@ func TestQueue(t *testing.T) { [][]byte{{1}, {2}, {4}}, [][]byte{{1}, {2}, {4}, nil, nil, nil}, }, + // let's play with lots.... + {lots, append(lots, nil)}, } for i, tc := range cases { diff --git a/state/span.go b/state/span.go new file mode 100644 index 000000000..5b9708417 --- /dev/null +++ b/state/span.go @@ -0,0 +1,123 @@ +package state + +import wire "github.com/tendermint/go-wire" + +var ( + keys = []byte("keys") + // uses dataKey from queue.go to prefix data +) + +// Span holds a number of different keys in a large range and allows +// use to make some basic range queries, like highest between, lowest between... +// All items are added with an index +// +// This becomes horribly inefficent as len(keys) => 1000+, but by then +// hopefully we have access to the iavl tree to do this well +// +// TODO: doesn't handle deleting.... +type Span struct { + store KVStore + // keys is sorted ascending and cannot contain duplicates + keys []uint64 +} + +// NewSpan loads or initializes a span of keys +func NewSpan(store KVStore) *Span { + s := &Span{store: store} + s.loadKeys() + return s +} + +// Set puts a value at a given height +func (s *Span) Set(h uint64, value []byte) { + key := makeKey(h) + s.store.Set(key, value) + s.addKey(h) + s.storeKeys() +} + +// Get returns the element at h if it exists +func (s *Span) Get(h uint64) []byte { + key := makeKey(h) + return s.store.Get(key) +} + +// Bottom returns the lowest element in the Span, along with its index +func (s *Span) Bottom() ([]byte, uint64) { + if len(s.keys) == 0 { + return nil, 0 + } + h := s.keys[0] + return s.Get(h), h +} + +// Top returns the highest element in the Span, along with its index +func (s *Span) Top() ([]byte, uint64) { + l := len(s.keys) + if l == 0 { + return nil, 0 + } + h := s.keys[l-1] + return s.Get(h), h +} + +// GTE returns the lowest element in the Span that is >= h, along with its index +func (s *Span) GTE(h uint64) ([]byte, uint64) { + for _, k := range s.keys { + if k >= h { + return s.Get(k), k + } + } + return nil, 0 +} + +// LTE returns the highest element in the Span that is <= h, +// along with its index +func (s *Span) LTE(h uint64) ([]byte, uint64) { + var k uint64 + // start from the highest and go down for the first match + for i := len(s.keys) - 1; i >= 0; i-- { + k = s.keys[i] + if k <= h { + return s.Get(k), k + } + } + return nil, 0 +} + +// addKey inserts this key, maintaining sorted order, no duplicates +func (s *Span) addKey(h uint64) { + for i, k := range s.keys { + // don't add duplicates + if h == k { + return + } + // insert before this key + if h < k { + // https://github.com/golang/go/wiki/SliceTricks + s.keys = append(s.keys, 0) + copy(s.keys[i+1:], s.keys[i:]) + s.keys[i] = h + return + } + } + // if it is higher than all (or empty keys), append + s.keys = append(s.keys, h) +} + +func (s *Span) loadKeys() { + b := s.store.Get(keys) + if b == nil { + return + } + err := wire.ReadBinaryBytes(b, &s.keys) + // hahaha... just like i love to hate :) + if err != nil { + panic(err) + } +} + +func (s *Span) storeKeys() { + b := wire.BinaryBytes(s.keys) + s.store.Set(keys, b) +} diff --git a/state/span_test.go b/state/span_test.go new file mode 100644 index 000000000..8362f3d90 --- /dev/null +++ b/state/span_test.go @@ -0,0 +1,122 @@ +package state + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +type kv struct { + k uint64 + v []byte +} + +type bscase struct { + data []kv + // these are the tests to try out + top kv + bottom kv + gets []kv // for each item check the query matches + lte []kv // value for lte queires... + gte []kv // value for gte +} + +func TestBasicSpan(t *testing.T) { + + a, b, c := []byte{0xaa}, []byte{0xbb}, []byte{0xcc} + + lots := make([]kv, 1000) + for i := range lots { + lots[i] = kv{uint64(3 * i), []byte{byte(i / 100), byte(i % 100)}} + } + + cases := []bscase{ + // simplest queries + { + []kv{{1, a}, {3, b}, {5, c}}, + kv{5, c}, + kv{1, a}, + []kv{{1, a}, {3, b}, {5, c}}, + []kv{{2, a}, {77, c}, {3, b}, {0, nil}}, // lte + []kv{{6, nil}, {2, b}, {1, a}}, // gte + }, + // add out of order + { + []kv{{7, a}, {2, b}, {6, c}}, + kv{7, a}, + kv{2, b}, + []kv{{2, b}, {6, c}, {7, a}}, + []kv{{4, b}, {7, a}, {1, nil}}, // lte + []kv{{4, c}, {7, a}, {1, b}}, // gte + }, + // add out of order and with duplicates + { + []kv{{7, a}, {2, b}, {6, c}, {7, c}, {6, b}, {2, a}}, + kv{7, c}, + kv{2, a}, + []kv{{2, a}, {6, b}, {7, c}}, + []kv{{5, a}, {6, b}, {123, c}}, // lte + []kv{{0, a}, {3, b}, {7, c}, {8, nil}}, // gte + }, + // try lots... + { + lots, + lots[len(lots)-1], + lots[0], + lots, + nil, + nil, + }, + } + + for i, tc := range cases { + store := NewMemKVStore() + + // initialize a queue and add items + s := NewSpan(store) + for _, x := range tc.data { + s.Set(x.k, x.v) + } + + testSpan(t, i, s, tc) + // reload and try the queries again + s2 := NewSpan(store) + testSpan(t, i+10, s2, tc) + } +} + +func testSpan(t *testing.T, idx int, s *Span, tc bscase) { + assert := assert.New(t) + i := strconv.Itoa(idx) + + v, k := s.Top() + assert.Equal(tc.top.k, k, i) + assert.Equal(tc.top.v, v, i) + + v, k = s.Bottom() + assert.Equal(tc.bottom.k, k, i) + assert.Equal(tc.bottom.v, v, i) + + for _, g := range tc.gets { + v = s.Get(g.k) + assert.Equal(g.v, v, i) + } + + for _, l := range tc.lte { + v, k = s.LTE(l.k) + assert.Equal(l.v, v, i) + if l.v != nil { + assert.True(k <= l.k, i) + } + } + + for _, t := range tc.gte { + v, k = s.GTE(t.k) + assert.Equal(t.v, v, i) + if t.v != nil { + assert.True(k >= t.k, i) + } + } + +}