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

MultiIndex primary key spec removal #569

Merged
merged 9 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
123 changes: 45 additions & 78 deletions packages/storage-plus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,14 @@ A `Map` key can be anything that implements the `PrimaryKey` trait. There are a
- `impl<'a, T: Endian + Clone> PrimaryKey<'a> for IntKey<T>`

That means that byte and string slices, byte vectors, and strings, can be conveniently used as keys.
Moreover, some other types can be used as well, like addresses and addresses references, pairs and triples, and
Moreover, some other types can be used as well, like addresses and address references, pairs and triples, and
integer types.

If the key represents and address, we suggest using `&Addr` for keys in storage, instead of `String` or string slices. This implies doing address validation
through `addr_validate` on any address passed in via a message, to ensure it's a legitimate address, and not random text
which will fail later.

Thus, `pub fn addr_validate(&self, &str) -> Addr` in `deps.api` can be used for address validation, and the returned
`Addr` can be conveniently used as key in a `Map` or similar structure.
If the key represents an address, we suggest using `&Addr` for keys in storage, instead of `String` or string slices.
This implies doing address validation through `addr_validate` on any address passed in via a message, to ensure it's a
legitimate address, and not random text which will fail later.
`pub fn addr_validate(&self, &str) -> Addr` in `deps.api` can be used for address validation, and the returned `Addr`
can then be conveniently used as key in a `Map` or similar structure.

### Composite Keys

Expand Down Expand Up @@ -265,7 +264,7 @@ reusing the calculated path to this key.
For simple keys, this is just a bit less typing and a bit less gas if you
use the same key for many calls. However, for composite keys, like
`(b"owner", b"spender")` it is **much** less typing. And highly recommended anywhere
you will use the a composite key even twice:
you will use a composite key even twice:

```rust
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
Expand Down Expand Up @@ -410,15 +409,13 @@ fn demo() -> StdResult<()> {

## IndexedMap

In cw-plus, there's currently one example of `IndexedMap` usage, in the `cw721-base` contract.
Let's use it to illustrate `IndexedMap` definition and usage.
Let's sue one example of `IndexedMap` definition and usage, originally taken from the `cw721-base` contract.

### Definition

```rust
pub struct TokenIndexes<'a> {
// pk goes to second tuple element
pub owner: MultiIndex<'a, (Addr, Vec<u8>), TokenInfo>,
pub owner: MultiIndex<'a, Addr, TokenInfo>,
}

impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
Expand All @@ -431,7 +428,7 @@ impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
let indexes = TokenIndexes {
owner: MultiIndex::new(
|d: &TokenInfo, k: Vec<u8>| (d.owner.clone(), k),
|d: &TokenInfo| d.owner.clone(),
"tokens",
"tokens__owner",
),
Expand All @@ -443,20 +440,19 @@ pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
Let's discuss this piece by piece:
```rust
pub struct TokenIndexes<'a> {
// pk goes to second tuple element
pub owner: MultiIndex<'a, (Addr, Vec<u8>), TokenInfo>,
pub owner: MultiIndex<'a, Addr, TokenInfo, String>,
}
```

These are the index definitions. Here there's only one index, called `owner`. There could be more, as public
members of the `TokenIndexes` struct.

We see that the `owner` index is a `MultiIndex`. A multi-index can have repeated values as keys. That's why
the primary key is being added as the last element of the multi-index key.
We see that the `owner` index is a `MultiIndex`. A multi-index can have repeated values as keys. The primary key is
used internally as the last element of the multi-index key, to disambiguate repeated index values.
Like the name implies, this is an index over tokens, by owner. Given that an owner can have multiple tokens,
we need a `MultiIndex` to be able to list / iterate over all the tokens a given owner has.

So, to recap, the `TokenInfo` data will originally be stored by `token_id` (which is a string value).
The `TokenInfo` data will originally be stored by `token_id` (which is a string value).
You can see this in the token creation code:
```rust
tokens().update(deps.storage, &msg.token_id, |old| match old {
Expand All @@ -466,46 +462,26 @@ You can see this in the token creation code:
```
(Incidentally, this is using `update` instead of `save`, to avoid overwriting an already existing token).

Then, it will be indexed by token `owner` (which is an `Addr`), so that we can list all the tokens an owner has.
That's why the `owner` index key is `(Addr, Vec<u8>)`. The first owned element is the `owner` data
, whereas the second one is the `token_id` (converted internally to `Vec<u8>`).
Given that `token_id` is a string value, we specify `String` as the last argument of the `MultiIndex` definition.
That way, the deserialization of the primary key will be done to the right type (an owned string).

The important thing here is that the key (and its components, in the case of a combined key) must implement
the `PrimaryKey` trait. You can see that the 2-tuple `(_, _)`, `Addr`, and `Vec<u8>` do implement `PrimaryKey`:
Then, this `TokenInfo` data will be indexed by token `owner` (which is an `Addr`). So that we can list all the tokens
an owner has. That's why the `owner` index key is `Addr`.

```rust
impl<'a, T: PrimaryKey<'a> + Prefixer<'a>, U: PrimaryKey<'a>> PrimaryKey<'a> for (T, U) {
type Prefix = T;
type SubPrefix = ();

fn key(&self) -> Vec<&[u8]> {
let mut keys = self.0.key();
keys.extend(&self.1.key());
keys
}
}
```
Other important thing here is that the key (and its components, in the case of a composite key) must implement
the `PrimaryKey` trait. You can see that `Addr` do implement `PrimaryKey`:

```rust
impl<'a> PrimaryKey<'a> for Addr {
type Prefix = ();
type SubPrefix = ();

fn key(&self) -> Vec<&[u8]> {
// this is simple, we don't add more prefixes
vec![self.as_bytes()]
}
}
```

```rust
impl<'a> PrimaryKey<'a> for Vec<u8> {
type Prefix = ();
type SubPrefix = ();

fn key(&self) -> Vec<&[u8]> {
vec![&self]
}
type Prefix = ();
type SubPrefix = ();
type Suffix = Self;
type SuperSuffix = Self;

fn key(&self) -> Vec<Key> {
// this is simple, we don't add more prefixes
vec![Key::Ref(self.as_bytes())]
}
}
```

Expand All @@ -525,15 +501,15 @@ impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
This implements the `IndexList` trait for `TokenIndexes`.
Note: this code is more or less boiler-plate, and needed for the internals. Do not try to customize this;
just return a list of all indexes.
Implementing this trait serves two purposes (which are really one, and the same): it allows the indexes
Implementing this trait serves two purposes (which are really one and the same): it allows the indexes
to be queried through `get_indexes`, and, it allows `TokenIndexes` to be treated as an `IndexList`. So that
it can be passed as a parameter during `IndexedMap` construction, below:

```rust
pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
let indexes = TokenIndexes {
owner: MultiIndex::new(
|d: &TokenInfo, k: Vec<u8>| (d.owner.clone(), k),
|d: &TokenInfo| d.owner.clone(),
"tokens",
"tokens__owner",
),
Expand All @@ -543,17 +519,15 @@ pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
```

Here `tokens()` is just a helper function, that simplifies the `IndexedMap` construction for us. First the
index (es) is (are) created, and then, the `IndexedMap` is created (using `IndexedMap::new`), and returned.
index (es) is (are) created, and then, the `IndexedMap` is created and returned.

During index creation, we must supply an index function per index
```rust
owner: MultiIndex::new(
|d: &TokenInfo, k: Vec<u8>| (d.owner.clone(), k),
owner: MultiIndex::new(|d: &TokenInfo| d.owner.clone(),
```

, which is the one that will take the value, and the primary key (which is always in `Vec<u8>` form) of the
original map, and create the index key from them. Of course, this requires that the elements required
for the index key are present in the value (which makes sense).
, which is the one that will take the value of the original map, and create the index key from it.
Of course, this requires that the elements required for the index key are present in the value.
Besides the index function, we must also supply the namespace of the pk, and the one for the new index.

---
Expand Down Expand Up @@ -595,7 +569,9 @@ Notice this uses `prefix()`, explained above in the `Map` section.
let tokens = res?;
```
Now `tokens` contains `(token_id, TokenInfo)` pairs for the given `owner`.
The pk values are `Vec<u8>`, as this is a limitation of the current implementation.
The pk values are `Vec<u8>` in the case of `prefix` + `range`, but will be deserialized to the proper type using
`prefix_de` + `range_de`; provided that the (optional) pk deserialization type (`String`, in this case)
is specified in the `MultiIndex` definition (see #Index keys deserialization, below).

Another example that is similar, but returning only the `token_id`s, using the `keys()` method:
```rust
Expand All @@ -612,22 +588,13 @@ Another example that is similar, but returning only the `token_id`s, using the `
.take(limit)
.collect();
```
Now `pks` contains `token_id` values (as `Vec<u8>`s) for the given `owner`.
Now `pks` contains `token_id` values (as raw `Vec<u8>`s) for the given `owner`. Again, by using `prefix_de` + `range_de`,
a deserialized key can be obtained instead, as detailed in the next section.

### Index keys deserialization

To deserialize keys of indexes (using the `*_de` functions), there are currently some requirements / limitations:

- For `UniqueIndex`: The primary key (`PK`) type needs to be specified, in order to deserialize the primary key to it.
This generic type comes with a default of `()`, which means that no deserialization / data will be provided
for the primary key. This is for backwards compatibility with the current `UniqueIndex` impl. It can also come handy
in cases you don't need the primary key, and are interested only in the deserialized value.

- For `MultiIndex`: The last element of the index tuple must be specified with the type you want it to be deserialized.
That is, the last tuple element serves as a marker for the deserialization type (in the same way `PK` does it in
`UniqueIndex`).

- There are currently some inconsistencies in the values that are returned for the different index keys. `MultiIndex`
returns a tuple with the remaining part of the index key along with the primary key, whereas `UniqueIndex` returns only
the primary key. This will be changed in the future (See https://github.com/CosmWasm/cw-plus/issues/532) for consistency
and compatibility with the base `Map` type behaviour.
For `UniqueIndex` and `MultiIndex`, the primary key (`PK`) type needs to be specified, in order to deserialize
the primary key to it. This generic type comes with a default of `()`, which means that no deserialization / data
will be provided for the primary key. This is for backwards compatibility with the current `UniqueIndex` / `MultiIndex`
impls. It can also come in handy in cases you don't need the primary key, and are interested only in the deserialized
values.
Loading