294 lines
9.2 KiB
Go
294 lines
9.2 KiB
Go
package ormtable
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/cosmos/cosmos-sdk/orm/internal/fieldnames"
|
|
|
|
"github.com/cosmos/cosmos-sdk/orm/encoding/encodeutil"
|
|
|
|
"google.golang.org/protobuf/reflect/protoregistry"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
|
|
ormv1 "github.com/cosmos/cosmos-sdk/api/cosmos/orm/v1"
|
|
|
|
"github.com/cosmos/cosmos-sdk/orm/encoding/ormkv"
|
|
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
|
|
)
|
|
|
|
const (
|
|
primaryKeyId uint32 = 0
|
|
indexIdLimit uint32 = 32768
|
|
seqId = indexIdLimit
|
|
)
|
|
|
|
// Options are options for building a Table.
|
|
type Options struct {
|
|
// Prefix is an optional prefix used to build the table's prefix.
|
|
Prefix []byte
|
|
|
|
// MessageType is the protobuf message type of the table.
|
|
MessageType protoreflect.MessageType
|
|
|
|
// TableDescriptor is an optional table descriptor to be explicitly used
|
|
// with the table. Generally this should be nil and the table descriptor
|
|
// should be pulled from the table message option. TableDescriptor
|
|
// cannot be used together with SingletonDescriptor.
|
|
TableDescriptor *ormv1.TableDescriptor
|
|
|
|
// SingletonDescriptor is an optional singleton descriptor to be explicitly used.
|
|
// Generally this should be nil and the table descriptor
|
|
// should be pulled from the singleton message option. SingletonDescriptor
|
|
// cannot be used together with TableDescriptor.
|
|
SingletonDescriptor *ormv1.SingletonDescriptor
|
|
|
|
// TypeResolver is an optional type resolver to be used when unmarshaling
|
|
// protobuf messages.
|
|
TypeResolver TypeResolver
|
|
|
|
// JSONValidator is an optional validator that can be used for validating
|
|
// messaging when using ValidateJSON. If it is nil, DefaultJSONValidator
|
|
// will be used
|
|
JSONValidator func(proto.Message) error
|
|
|
|
// BackendResolver is an optional function which retrieves a Backend from the context.
|
|
// If it is nil, the default behavior will be to attempt to retrieve a
|
|
// backend using the method that WrapContextDefault uses. This method
|
|
// can be used to implement things like "store keys" which would allow a
|
|
// table to only be used with a specific backend and to hide direct
|
|
// access to the backend other than through the table interface.
|
|
// Mutating operations will attempt to cast ReadBackend to Backend and
|
|
// will return an error if that fails.
|
|
BackendResolver BackendResolver
|
|
}
|
|
|
|
// TypeResolver is an interface that can be used for the protoreflect.UnmarshalOptions.Resolver option.
|
|
type TypeResolver interface {
|
|
protoregistry.MessageTypeResolver
|
|
protoregistry.ExtensionTypeResolver
|
|
}
|
|
|
|
// Build builds a Table instance from the provided Options.
|
|
func Build(options Options) (Table, error) {
|
|
messageDescriptor := options.MessageType.Descriptor()
|
|
|
|
backendResolver := options.BackendResolver
|
|
if backendResolver == nil {
|
|
backendResolver = getBackendDefault
|
|
}
|
|
|
|
table := &tableImpl{
|
|
primaryKeyIndex: &primaryKeyIndex{
|
|
indexers: []indexer{},
|
|
getBackend: backendResolver,
|
|
},
|
|
indexes: []Index{},
|
|
indexesByFields: map[fieldnames.FieldNames]concreteIndex{},
|
|
uniqueIndexesByFields: map[fieldnames.FieldNames]UniqueIndex{},
|
|
entryCodecsById: map[uint32]ormkv.EntryCodec{},
|
|
indexesById: map[uint32]Index{},
|
|
typeResolver: options.TypeResolver,
|
|
customJSONValidator: options.JSONValidator,
|
|
}
|
|
|
|
pkIndex := table.primaryKeyIndex
|
|
|
|
tableDesc := options.TableDescriptor
|
|
if tableDesc == nil {
|
|
tableDesc = proto.GetExtension(messageDescriptor.Options(), ormv1.E_Table).(*ormv1.TableDescriptor)
|
|
}
|
|
|
|
singletonDesc := options.SingletonDescriptor
|
|
if singletonDesc == nil {
|
|
singletonDesc = proto.GetExtension(messageDescriptor.Options(), ormv1.E_Singleton).(*ormv1.SingletonDescriptor)
|
|
}
|
|
|
|
switch {
|
|
case tableDesc != nil:
|
|
if singletonDesc != nil {
|
|
return nil, ormerrors.InvalidTableDefinition.Wrapf("message %s cannot be declared as both a table and a singleton", messageDescriptor.FullName())
|
|
}
|
|
case singletonDesc != nil:
|
|
if singletonDesc.Id == 0 {
|
|
return nil, ormerrors.InvalidTableId.Wrapf("%s", messageDescriptor.FullName())
|
|
}
|
|
|
|
prefix := encodeutil.AppendVarUInt32(options.Prefix, singletonDesc.Id)
|
|
pkCodec, err := ormkv.NewPrimaryKeyCodec(
|
|
prefix,
|
|
options.MessageType,
|
|
nil,
|
|
proto.UnmarshalOptions{Resolver: options.TypeResolver},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pkIndex.PrimaryKeyCodec = pkCodec
|
|
table.tablePrefix = prefix
|
|
table.tableId = singletonDesc.Id
|
|
|
|
return &singleton{table}, nil
|
|
default:
|
|
return nil, ormerrors.InvalidTableDefinition.Wrapf("missing table descriptor for %s", messageDescriptor.FullName())
|
|
}
|
|
|
|
tableId := tableDesc.Id
|
|
if tableId == 0 {
|
|
return nil, ormerrors.InvalidTableId.Wrapf("table %s", messageDescriptor.FullName())
|
|
}
|
|
|
|
prefix := options.Prefix
|
|
prefix = encodeutil.AppendVarUInt32(prefix, tableId)
|
|
table.tablePrefix = prefix
|
|
table.tableId = tableId
|
|
|
|
if tableDesc.PrimaryKey == nil {
|
|
return nil, ormerrors.MissingPrimaryKey.Wrap(string(messageDescriptor.FullName()))
|
|
}
|
|
|
|
pkFields := fieldnames.CommaSeparatedFieldNames(tableDesc.PrimaryKey.Fields)
|
|
table.primaryKeyIndex.fields = pkFields
|
|
pkFieldNames := pkFields.Names()
|
|
if len(pkFieldNames) == 0 {
|
|
return nil, ormerrors.InvalidTableDefinition.Wrapf("empty primary key fields for %s", messageDescriptor.FullName())
|
|
}
|
|
|
|
pkPrefix := encodeutil.AppendVarUInt32(prefix, primaryKeyId)
|
|
pkCodec, err := ormkv.NewPrimaryKeyCodec(
|
|
pkPrefix,
|
|
options.MessageType,
|
|
pkFieldNames,
|
|
proto.UnmarshalOptions{Resolver: options.TypeResolver},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pkIndex.PrimaryKeyCodec = pkCodec
|
|
table.indexesByFields[pkFields] = pkIndex
|
|
table.uniqueIndexesByFields[pkFields] = pkIndex
|
|
table.entryCodecsById[primaryKeyId] = pkIndex
|
|
table.indexesById[primaryKeyId] = pkIndex
|
|
table.indexes = append(table.indexes, pkIndex)
|
|
|
|
for _, idxDesc := range tableDesc.Index {
|
|
id := idxDesc.Id
|
|
if id == 0 || id >= indexIdLimit {
|
|
return nil, ormerrors.InvalidIndexId.Wrapf("index on table %s with fields %s, invalid id %d", messageDescriptor.FullName(), idxDesc.Fields, id)
|
|
}
|
|
|
|
if _, ok := table.entryCodecsById[id]; ok {
|
|
return nil, ormerrors.DuplicateIndexId.Wrapf("id %d on table %s", id, messageDescriptor.FullName())
|
|
}
|
|
|
|
idxFields := fieldnames.CommaSeparatedFieldNames(idxDesc.Fields)
|
|
idxPrefix := encodeutil.AppendVarUInt32(prefix, id)
|
|
var index concreteIndex
|
|
|
|
// altNames contains all the alternative "names" of this index
|
|
altNames := map[fieldnames.FieldNames]bool{idxFields: true}
|
|
|
|
if idxDesc.Unique && isNonTrivialUniqueKey(idxFields.Names(), pkFieldNames) {
|
|
uniqCdc, err := ormkv.NewUniqueKeyCodec(
|
|
idxPrefix,
|
|
options.MessageType,
|
|
idxFields.Names(),
|
|
pkFieldNames,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uniqIdx := &uniqueKeyIndex{
|
|
UniqueKeyCodec: uniqCdc,
|
|
fields: idxFields,
|
|
primaryKey: pkIndex,
|
|
getReadBackend: backendResolver,
|
|
}
|
|
table.uniqueIndexesByFields[idxFields] = uniqIdx
|
|
index = uniqIdx
|
|
} else {
|
|
idxCdc, err := ormkv.NewIndexKeyCodec(
|
|
idxPrefix,
|
|
options.MessageType,
|
|
idxFields.Names(),
|
|
pkFieldNames,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index = &indexKeyIndex{
|
|
IndexKeyCodec: idxCdc,
|
|
fields: idxFields,
|
|
primaryKey: pkIndex,
|
|
getReadBackend: backendResolver,
|
|
}
|
|
|
|
// non-unique indexes can sometimes be named by several sub-lists of
|
|
// fields and we need to handle all of them. For example consider,
|
|
// a primary key for fields "a,b,c" and an index on field "c". Because the
|
|
// rest of the primary key gets appended to the index key, the index for "c"
|
|
// is actually stored as "c,a,b". So this index can be referred to
|
|
// by the fields "c", "c,a", or "c,a,b".
|
|
allFields := index.GetFieldNames()
|
|
allFieldNames := fieldnames.FieldsFromNames(allFields)
|
|
altNames[allFieldNames] = true
|
|
for i := 1; i < len(allFields); i++ {
|
|
altName := fieldnames.FieldsFromNames(allFields[:i])
|
|
if altNames[altName] {
|
|
continue
|
|
}
|
|
|
|
// we check by generating a codec for each sub-list of fields,
|
|
// then we see if the full list of fields matches.
|
|
altIdxCdc, err := ormkv.NewIndexKeyCodec(
|
|
idxPrefix,
|
|
options.MessageType,
|
|
allFields[:i],
|
|
pkFieldNames,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fieldnames.FieldsFromNames(altIdxCdc.GetFieldNames()) == allFieldNames {
|
|
altNames[altName] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
for name := range altNames {
|
|
if _, ok := table.indexesByFields[name]; ok {
|
|
return nil, fmt.Errorf("duplicate index for fields %s", name)
|
|
}
|
|
|
|
table.indexesByFields[name] = index
|
|
}
|
|
|
|
table.entryCodecsById[id] = index
|
|
table.indexesById[id] = index
|
|
table.indexes = append(table.indexes, index)
|
|
table.indexers = append(table.indexers, index.(indexer))
|
|
}
|
|
|
|
if tableDesc.PrimaryKey.AutoIncrement {
|
|
autoIncField := pkCodec.GetFieldDescriptors()[0]
|
|
if len(pkFieldNames) != 1 && autoIncField.Kind() != protoreflect.Uint64Kind {
|
|
return nil, ormerrors.InvalidAutoIncrementKey.Wrapf("field %s", autoIncField.FullName())
|
|
}
|
|
|
|
seqPrefix := encodeutil.AppendVarUInt32(prefix, seqId)
|
|
seqCodec := ormkv.NewSeqCodec(options.MessageType, seqPrefix)
|
|
table.entryCodecsById[seqId] = seqCodec
|
|
return &autoIncrementTable{
|
|
tableImpl: table,
|
|
autoIncField: autoIncField,
|
|
seqCodec: seqCodec,
|
|
}, nil
|
|
}
|
|
|
|
return table, nil
|
|
}
|