- type safe management of state
- multipart keys
- secondary indexes
- unique indexes
- easy prefix and range queries
- automatic genesis import/export
- automatic query services for clients, including support for light client proofs (still in development)
- indexing state data in external databases (still in development)
Design and Philosophy
The ORM’s data model is inspired by the relational data model found in SQL databases. The core abstraction is a table with a primary key and optional secondary indexes. Because the Cosmos SDK uses protobuf as its encoding layer, ORM tables are defined directly in .proto files using protobuf options. Each table is defined by a single protobufmessage
type and a schema of multiple tables is
represented by a single .proto file.
Table structure is specified in the same file where messages are defined in order to make it easy to focus on better
design of the state layer. Because blockchain state layout is part of the public API for clients (TODO: link to docs on
light client proofs), it is important to think about the state layout as being part of the public API of a module.
Changing the state layout actually breaks clients, so it is ideal to think through it carefully up front and to aim for
a design that will eliminate or minimize breaking changes down the road. Also, good design of state enables building
more performant and sophisticated applications. Providing users with a set of tools inspired by relational databases
which have a long history of database design best practices and allowing schema to be specified declaratively in a
single place are design choices the ORM makes to enable better design and more durable APIs.
Also, by only supporting the table abstraction as opposed to key-value pair maps, it is easy to add to new
columns/fields to any data structure without causing a breaking change and the data structures can easily be indexed in
any off-the-shelf SQL database for more sophisticated queries.
The encoding of fields in keys is designed to support ordered iteration for all protobuf primitive field types
except for bytes
as well as the well-known types google.protobuf.Timestamp
and google.protobuf.Duration
. Encodings
are optimized for storage space when it makes sense (see the documentation in cosmos/orm/v1/orm.proto
for more details)
and table rows do not use extra storage space to store key fields in the value.
We recommend that users of the ORM attempt to follow database design best practices such as
normalization (at least 1NF).
For instance, defining repeated
fields in a table is considered an anti-pattern because breaks first normal form (1NF).
Although we support repeated
fields in tables, they cannot be used as key fields for this reason. This may seem
restrictive but years of best practice (and also experience in the SDK) have shown that following this pattern
leads to easier to maintain schemas.
To illustrate the motivation for these principles with an example from the SDK, historically balances were stored
as a mapping from account -> map of denom to amount. This did not scale well because an account with 100 token balances
needed to be encoded/decoded every time a single coin balance changed. Now balances are stored as account,denom -> amount
as in the example above. With the ORM’s data model, if we wanted to add a new field to Balance
such as
unlocked_balance
(if vesting accounts were redesigned in this way), it would be easy to add it to this table without
requiring a data migration. Because of the ORM’s optimizations, the account and denom are only stored in the key part
of storage and not in the value leading to both a flexible data model and efficient usage of storage.
Defining Tables
To define a table:- create a .proto file to describe the module’s state (naming it
state.proto
is recommended for consistency), and import “cosmos/orm/v1/orm.proto”, ex:
- define a
message
for the table, ex:
- add the
cosmos.orm.v1.table
option to the table and give the table anid
unique within this .proto file:
- define the primary key field or fields, as a comma-separated list of the fields from the message which should make up the primary key:
- add any desired secondary indexes by specifying an
id
unique within the table and a comma-separate list of the index fields:
Auto-incrementing Primary Keys
A common pattern in SDK modules and in database design is to define tables with a single integerid
field with an
automatically generated primary key. In the ORM we can do this by setting the auto_increment
option to true
on the
primary key, ex:
Unique Indexes
A unique index can be added by setting theunique
option to true
on an index, ex:
Singletons
The ORM also supports a special type of table with only one row called asingleton
. This can be used for storing
module parameters. Singletons only need to define a unique id
and that cannot conflict with the id of other
tables or singletons in the same .proto file. Ex:
Running Codegen
NOTE: the ORM will only work with protobuf code that implements the google.golang.org/protobuf API. That means it will not work with code generated using gogo-proto. To install the ORM’s code generator, run:buf.gen.yaml
that runs protoc-gen-go
, protoc-gen-go-grpc
and protoc-gen-go-cosmos-orm
using buf managed mode:
Using the ORM in a module
Initialization
To use the ORM in a module, first create aModuleSchemaDescriptor
. This tells the ORM which .proto files have defined
an ORM schema and assigns them all a unique non-zero id. Ex:
state.proto
, there should be an interface StateStore
that got generated
with a constructor NewStateStore
that takes a parameter of type ormdb.ModuleDB
. Add a reference to StateStore
to your module’s keeper struct. Ex:
StateStore
instance via an ormdb.ModuleDB
that is instantiated from the SchemaDescriptor
above and one or more store services from cosmossdk.io/core/store
. Ex:
Using the generated code
The generated code for the ORM contains methods for inserting, updating, deleting and querying table entries. For each table in a .proto file, there is a type-safe table interface implemented in generated code. For instance, for a table namedBalance
there should be a BalanceTable
interface that looks like this:
BalanceTable
should be accessible from the StateStore
interface (assuming our file is named state.proto
)
via a BalanceTable()
accessor method. If all the above example tables/singletons were in the same state.proto
,
then StateStore
would get generated like this:
BalanceTable
in a keeper method we could use code like this:
List
methods take IndexKey
parameters. For instance, BalanceTable.List
takes BalanceIndexKey
. BalanceIndexKey
let’s represent index keys for the different indexes (primary and secondary) on the Balance
table. The primary key
in the Balance
table gets a struct BalanceAccountDenomIndexKey
and the first index gets an index key BalanceDenomIndexKey
.
If we wanted to list all the denoms and amounts that an account holds, we would use BalanceAccountDenomIndexKey
with a List
query just on the account prefix. Ex: