Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Table-Store (aka ORM) package - Table and Indexable #9751

Merged
merged 35 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5539498
WIP on adding table/indexable
blushi Jul 8, 2021
10a3bec
Add some tests
blushi Jul 20, 2021
86a5ac6
Add more tests
blushi Jul 22, 2021
580c41b
Add sequence
blushi Jul 22, 2021
d0a77b6
Update testdata
blushi Jul 22, 2021
fc72796
Lint
blushi Jul 22, 2021
71cac77
Merge branch 'master' into marie/9237-table-store-1
blushi Jul 22, 2021
a4a4e09
Add docs
blushi Jul 22, 2021
b6f2d08
Update docs
blushi Jul 22, 2021
ed2a1d2
Use Update instead of Save
blushi Jul 28, 2021
b0a8586
Merge branch 'master' into marie/9237-table-store-1
blushi Jul 28, 2021
30c765b
Move orm to x/group
blushi Aug 11, 2021
5e71573
Add orm
blushi Aug 11, 2021
63168ed
Merge branch 'master' into marie/9237-table-store-1
blushi Aug 11, 2021
482617d
Merge branch 'master' into marie/9237-table-store-1
blushi Sep 2, 2021
09d832a
Merge branch 'master' into marie/9237-table-store-1
blushi Oct 7, 2021
0151b74
Update orm with latest changes
blushi Oct 8, 2021
4d2486a
Mv orm to x/group/internal
blushi Oct 8, 2021
a368554
Update go.mod and fix tests
blushi Oct 8, 2021
ce21252
Update README
blushi Oct 8, 2021
8956be8
Merge branch 'master' into marie/9237-table-store-1
blushi Oct 8, 2021
560662e
Fix tests
blushi Oct 8, 2021
bb495f7
Use [2]byte for table prefix key
blushi Oct 8, 2021
915d734
Update docs
blushi Oct 8, 2021
0ef7f82
Merge branch 'master' into marie/9237-table-store-1
blushi Oct 8, 2021
e3aa3e8
Rm file
blushi Oct 8, 2021
df0c304
Rm file
blushi Oct 8, 2021
661d449
Revert store/README
blushi Oct 8, 2021
b47264d
Register errors in types/errors
blushi Oct 8, 2021
00b97d8
Fix group err
blushi Oct 8, 2021
61cbb3c
Merge branch 'master' into marie/9237-table-store-1
blushi Oct 14, 2021
a8b57d2
Simplify table creation
blushi Oct 21, 2021
7c63cbd
Merge branch 'master' into marie/9237-table-store-1
blushi Oct 21, 2021
87fc7b0
Update x/group/internal/orm/table.go
blushi Oct 21, 2021
d2f9a52
Address review comments
blushi Oct 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/core/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ When `Store.{Get, Set}()` is called, the store forwards the call to its parent,

When `Store.Iterator()` is called, it does not simply prefix the `Store.prefix`, since it does not work as intended. In that case, some of the elements are traversed even they are not starting with the prefix.

## Table Store
blushi marked this conversation as resolved.
Show resolved Hide resolved

The table-store package provides a framework for creating relational database tables with primary and secondary keys.

```go
type Table struct {
model reflect.Type
prefix byte
afterSave []AfterSaveInterceptor
afterDelete []AfterDeleteInterceptor
cdc codec.Codec
}
```

Such table can be built given a `codec.ProtoMarshaler` model type, a prefix to access the underlying prefix store used to store table data as well as a `Codec` for marshalling/unmarshalling.
In the prefix store, entities are stored by an unique identifier called `RowID` which can be based either on an `uint64` auto-increment counter or dynamic size bytes.
Regular CRUD operations can be performed on a table, these methods take a `sdk.KVStore` as parameter to get the table prefix store.

## Next {hide}

Learn about [encoding](./encoding.md) {hide}
20 changes: 20 additions & 0 deletions store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,23 @@ type Store struct {
```

`Store.Store` is a `dbadapter.Store` with a `dbm.NewMemDB()`. All `KVStore` methods are reused. When `Store.Commit()` is called, new `dbadapter.Store` is assigned, discarding previous reference and making it garbage collected.

## Table
blushi marked this conversation as resolved.
Show resolved Hide resolved

The table-store package provides a framework for creating relational database tables with primary and secondary keys.

```go
type Table struct {
model reflect.Type
prefix byte
afterSave []AfterSaveInterceptor
afterDelete []AfterDeleteInterceptor
cdc codec.Codec
}
```

Such table can be built given a `codec.ProtoMarshaler` model type, a prefix to access the underlying prefix store used to store table data as well as a `Codec` for marshalling/unmarshalling.
In the prefix store, entities are stored by an unique identifier called `RowID` which can be based either on an `uint64` auto-increment counter or dynamic size bytes.
Regular CRUD operations can be performed on a table, these methods take a `sdk.KVStore` as parameter to get the table prefix store.


37 changes: 37 additions & 0 deletions store/table/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package table

// PrefixRange turns a prefix into a (start, end) range. The start is the given prefix value and
// the end is calculated by adding 1 bit to the start value. Nil is not allowed as prefix.
// Example: []byte{1, 3, 4} becomes []byte{1, 3, 5}
// []byte{15, 42, 255, 255} becomes []byte{15, 43, 0, 0}
//
// In case of an overflow the end is set to nil.
// Example: []byte{255, 255, 255, 255} becomes nil
//
func PrefixRange(prefix []byte) ([]byte, []byte) {
if prefix == nil {
panic("nil key not allowed")
}
// special case: no prefix is whole range
if len(prefix) == 0 {
return nil, nil
}

// copy the prefix and update last byte
end := make([]byte, len(prefix))
copy(end, prefix)
l := len(end) - 1
end[l]++

// wait, what if that overflowed?....
for end[l] == 0 && l > 0 {
l--
end[l]++
}

// okay, funny guy, you gave us FFF, no end to this range...
if l == 0 && end[0] == 0 {
end = nil
}
return prefix, end
}
66 changes: 66 additions & 0 deletions store/table/index_key_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package table

// Max255DynamicLengthIndexKeyCodec works with up to 255 byte dynamic size RowIDs.
// They are encoded as `concat(searchableKey, rowID, len(rowID)[0])`.
blushi marked this conversation as resolved.
Show resolved Hide resolved
type Max255DynamicLengthIndexKeyCodec struct{}

// BuildIndexKey builds the index key by appending searchableKey with rowID and length int.
// The RowID length must not be greater than 255.
func (Max255DynamicLengthIndexKeyCodec) BuildIndexKey(searchableKey []byte, rowID RowID) []byte {
rowIDLen := len(rowID)
switch {
case rowIDLen == 0:
panic("Empty RowID")
case rowIDLen > 255:
panic("RowID exceeds max size")
}

searchableKeyLen := len(searchableKey)
res := make([]byte, searchableKeyLen+rowIDLen+1)
copy(res, searchableKey)
copy(res[searchableKeyLen:], rowID)
res[searchableKeyLen+rowIDLen] = byte(rowIDLen)
return res
}

// StripRowID returns the RowID from the combined persistentIndexKey. It is the reverse operation to BuildIndexKey
// but with the searchableKey and length int dropped.
func (Max255DynamicLengthIndexKeyCodec) StripRowID(persistentIndexKey []byte) RowID {
n := len(persistentIndexKey)
searchableKeyLen := persistentIndexKey[n-1]
return persistentIndexKey[n-int(searchableKeyLen)-1 : n-1]
}

// FixLengthIndexKeyCodec expects the RowID to always have the same length with all entries.
// They are encoded as `concat(searchableKey, rowID)`.
type FixLengthIndexKeyCodec struct {
rowIDLength int
}

// FixLengthIndexKeys is a constructor for FixLengthIndexKeyCodec.
func FixLengthIndexKeys(rowIDLength int) *FixLengthIndexKeyCodec {
return &FixLengthIndexKeyCodec{rowIDLength: rowIDLength}
}

// BuildIndexKey builds the index key by appending searchableKey with rowID.
// The RowID length must not be greater than what is defined by rowIDLength in construction.
func (c FixLengthIndexKeyCodec) BuildIndexKey(searchableKey []byte, rowID RowID) []byte {
switch n := len(rowID); {
case n == 0:
panic("Empty RowID")
case n > c.rowIDLength:
panic("RowID exceeds max size")
}
n := len(searchableKey)
res := make([]byte, n+c.rowIDLength)
copy(res, searchableKey)
copy(res[n:], rowID)
return res
}

// StripRowID returns the RowID from the combined persistentIndexKey. It is the reverse operation to BuildIndexKey
// but with the searchableKey dropped.
func (c FixLengthIndexKeyCodec) StripRowID(persistentIndexKey []byte) RowID {
n := len(persistentIndexKey)
return persistentIndexKey[n-c.rowIDLength:]
}
115 changes: 115 additions & 0 deletions store/table/index_key_codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package table

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEncodeIndexKey(t *testing.T) {
specs := map[string]struct {
srcKey []byte
srcRowID RowID
enc IndexKeyCodec
expKey []byte
expPanic bool
}{
"dynamic length example 1": {
srcKey: []byte{0x0, 0x1, 0x2},
srcRowID: []byte{0x3, 0x4},
enc: Max255DynamicLengthIndexKeyCodec{},
expKey: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x2},
},
"dynamic length example 2": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte{0x2, 0x3, 0x4},
enc: Max255DynamicLengthIndexKeyCodec{},
expKey: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x3},
},
"dynamic length max row ID": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte(strings.Repeat("a", 255)),
enc: Max255DynamicLengthIndexKeyCodec{},
expKey: append(append([]byte{0x0, 0x1}, []byte(strings.Repeat("a", 255))...), 0xff),
},
"dynamic length panics with empty rowID": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte{},
enc: Max255DynamicLengthIndexKeyCodec{},
expPanic: true,
},
"dynamic length exceeds max row ID": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte(strings.Repeat("a", 256)),
enc: Max255DynamicLengthIndexKeyCodec{},
expPanic: true,
},
"uint64 example": {
srcKey: []byte{0x0, 0x1, 0x2},
srcRowID: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
enc: FixLengthIndexKeys(8),
expKey: []byte{0x0, 0x1, 0x2, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
},
"uint64 panics with empty rowID": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte{},
enc: FixLengthIndexKeys(8),
expPanic: true,
},
"uint64 exceeds max bytes in rowID": {
srcKey: []byte{0x0, 0x1},
srcRowID: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9},
enc: FixLengthIndexKeys(8),
expPanic: true,
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
if spec.expPanic {
require.Panics(t,
func() {
_ = spec.enc.BuildIndexKey(spec.srcKey, spec.srcRowID)
})
return
}
got := spec.enc.BuildIndexKey(spec.srcKey, spec.srcRowID)
assert.Equal(t, spec.expKey, got)
})
}
}
func TestDecodeIndexKey(t *testing.T) {
specs := map[string]struct {
srcKey []byte
enc IndexKeyCodec
expRowID RowID
}{
"dynamic length example 1": {
srcKey: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x2},
enc: Max255DynamicLengthIndexKeyCodec{},
expRowID: []byte{0x3, 0x4},
},
"dynamic length example 2": {
srcKey: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x3},
enc: Max255DynamicLengthIndexKeyCodec{},
expRowID: []byte{0x2, 0x3, 0x4},
},
"dynamic length max row ID": {
srcKey: append(append([]byte{0x0, 0x1}, []byte(strings.Repeat("a", 255))...), 0xff),
enc: Max255DynamicLengthIndexKeyCodec{},
expRowID: []byte(strings.Repeat("a", 255)),
},
"uint64 example": {
srcKey: []byte{0x0, 0x1, 0x2, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
expRowID: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8},
enc: FixLengthIndexKeys(8),
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
gotRow := spec.enc.StripRowID(spec.srcKey)
assert.Equal(t, spec.expRowID, gotRow)
})
}
}
39 changes: 39 additions & 0 deletions store/table/index_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package table

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPrefixRange(t *testing.T) {
cases := map[string]struct {
src []byte
expStart []byte
expEnd []byte
expPanic bool
}{
"normal": {src: []byte{1, 3, 4}, expStart: []byte{1, 3, 4}, expEnd: []byte{1, 3, 5}},
"normal short": {src: []byte{79}, expStart: []byte{79}, expEnd: []byte{80}},
"empty case": {src: []byte{}},
"roll-over example 1": {src: []byte{17, 28, 255}, expStart: []byte{17, 28, 255}, expEnd: []byte{17, 29, 0}},
"roll-over example 2": {src: []byte{15, 42, 255, 255}, expStart: []byte{15, 42, 255, 255}, expEnd: []byte{15, 43, 0, 0}},
"pathological roll-over": {src: []byte{255, 255, 255, 255}, expStart: []byte{255, 255, 255, 255}},
"nil prohibited": {expPanic: true},
}

for testName, tc := range cases {
t.Run(testName, func(t *testing.T) {
if tc.expPanic {
require.Panics(t, func() {
PrefixRange(tc.src)
})
return
}
start, end := PrefixRange(tc.src)
assert.Equal(t, tc.expStart, start)
assert.Equal(t, tc.expEnd, end)
})
}
}
81 changes: 81 additions & 0 deletions store/table/sequence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package table

import (
"encoding/binary"

"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/errors"
)

// sequenceStorageKey is a fix key to read/ write data on the storage layer
var sequenceStorageKey = []byte{0x1}

// sequence is a persistent unique key generator based on a counter.
type Sequence struct {
prefix byte
}

func NewSequence(prefix byte) Sequence {
return Sequence{
prefix: prefix,
}
}

// NextVal increments and persists the counter by one and returns the value.
func (s Sequence) NextVal(store sdk.KVStore) uint64 {
pStore := prefix.NewStore(store, []byte{s.prefix})
v := pStore.Get(sequenceStorageKey)
seq := DecodeSequence(v)
seq++
pStore.Set(sequenceStorageKey, EncodeSequence(seq))
return seq
}

// CurVal returns the last value used. 0 if none.
func (s Sequence) CurVal(store sdk.KVStore) uint64 {
pStore := prefix.NewStore(store, []byte{s.prefix})
v := pStore.Get(sequenceStorageKey)
return DecodeSequence(v)
}

// PeekNextVal returns the CurVal + increment step. Not persistent.
func (s Sequence) PeekNextVal(store sdk.KVStore) uint64 {
pStore := prefix.NewStore(store, []byte{s.prefix})
v := pStore.Get(sequenceStorageKey)
return DecodeSequence(v) + 1
}

// InitVal sets the start value for the sequence. It must be called only once on an empty DB.
// Otherwise an error is returned when the key exists. The given start value is stored as current
// value.
//
// It is recommended to call this method only for a sequence start value other than `1` as the
// method consumes unnecessary gas otherwise. A scenario would be an import from genesis.
func (s Sequence) InitVal(store sdk.KVStore, seq uint64) error {
pStore := prefix.NewStore(store, []byte{s.prefix})
if pStore.Has(sequenceStorageKey) {
return errors.Wrap(ErrUniqueConstraint, "already initialized")
}
pStore.Set(sequenceStorageKey, EncodeSequence(seq))
return nil
}

// DecodeSequence converts the binary representation into an Uint64 value.
func DecodeSequence(bz []byte) uint64 {
if bz == nil {
return 0
}
val := binary.BigEndian.Uint64(bz)
return val
}

// EncodedSeqLength number of bytes used for the binary representation of a sequence value.
const EncodedSeqLength = 8

// EncodeSequence converts the sequence value into the binary representation.
func EncodeSequence(val uint64) []byte {
bz := make([]byte, EncodedSeqLength)
binary.BigEndian.PutUint64(bz, val)
return bz
}
Loading