diff --git a/docs-test-gen/Cargo.lock b/docs-test-gen/Cargo.lock index 25f0f8df..c6877c5d 100644 --- a/docs-test-gen/Cargo.lock +++ b/docs-test-gen/Cargo.lock @@ -254,6 +254,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-storage-macro" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90e7828ea0175c45743178f8b0290513752e949b2fdfa5bda52a7389d732610" +dependencies = [ + "syn 2.0.66", +] + [[package]] name = "cw-storage-plus" version = "2.0.0" @@ -261,6 +270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" dependencies = [ "cosmwasm-std", + "cw-storage-macro", "schemars", "serde", ] diff --git a/docs-test-gen/Cargo.toml b/docs-test-gen/Cargo.toml index 8f77c380..643b3c3a 100644 --- a/docs-test-gen/Cargo.toml +++ b/docs-test-gen/Cargo.toml @@ -13,10 +13,10 @@ strum = { version = "0.26.2", features = ["derive"] } [dev-dependencies] cw2 = "*" +cw-storage-plus = { version = "*", features = ["macro"]} cosmwasm-schema = "*" cosmwasm-std = { version = "*", features = ["stargate", "staking", "cosmwasm_2_0"] } sha2 = "0.10.8" cosmos-sdk-proto = { version = "0.21.1", default-features = false } # Used in IBC code -cw-storage-plus = "*" serde = "*" cw-storey = "*" diff --git a/docs-test-gen/templates/storage.tpl b/docs-test-gen/templates/storage.tpl index 060159e5..8a0cd7d5 100644 --- a/docs-test-gen/templates/storage.tpl +++ b/docs-test-gen/templates/storage.tpl @@ -1,15 +1,91 @@ #[allow(unused_imports)] mod imports { - pub use cosmwasm_std::*; pub use cosmwasm_schema::cw_serde; + pub use cosmwasm_std::*; } #[allow(unused_imports)] use imports::*; +#[allow(dead_code, unused_variables)] +mod users { + use super::*; + + use cw_storage_plus::{index_list, IndexedMap, UniqueIndex}; + + pub type Handle = String; + + #[cw_serde] + pub struct User { + pub handle: String, + pub country: String, + } + + pub struct ExampleUsers { + pub alice: User, + pub bob: User, + } + + pub fn example_users() -> ExampleUsers { + ExampleUsers { + alice: User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + bob: User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + } + } + + #[index_list(User)] + pub struct UserIndexes<'a> { + pub handle_ix: UniqueIndex<'a, Handle, User, Addr>, + } + + pub fn user_indexes(prefix: &'static str) -> UserIndexes<'static> { + UserIndexes { + handle_ix: UniqueIndex::new(|user| user.handle.clone(), prefix), + } + } +} + #[test] fn doctest() { - #[allow(unused_mut)] - let mut storage = cosmwasm_std::testing::MockStorage::new(); - {{code}} + #[allow(unused_variables, unused_mut)] + let mut storage = cosmwasm_std::testing::MockStorage::new(); + + let users = cw_storage_plus::IndexedMap::::new("uu", users::user_indexes("uuh")); + + let users_data = [ + ( + Addr::unchecked("aaa"), + users::User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + ), + ( + Addr::unchecked("bbb"), + users::User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + ), + ( + Addr::unchecked("ccc"), + users::User { + handle: "carol".to_string(), + country: "UK".to_string(), + }, + ), + ]; + + for (addr, user) in users_data { + users.save(&mut storage, addr, &user).unwrap(); + } + + #[rustfmt::skip] + {{code}} } diff --git a/src/pages/_meta.json b/src/pages/_meta.json index 3003fdee..3c198dfc 100644 --- a/src/pages/_meta.json +++ b/src/pages/_meta.json @@ -3,7 +3,7 @@ "core": "CosmWasm Core", "ibc": "IBC", "sylvia": "Sylvia", - "cw-storage-plus": "cw-storage-plus", + "cw-storage-plus": "StoragePlus", "cw-multi-test": "MultiTest", "how-to-doc": "How to doc", "tags": { diff --git a/src/pages/cw-storage-plus/containers/_meta.json b/src/pages/cw-storage-plus/containers/_meta.json index c9ba679d..f87bc0d8 100644 --- a/src/pages/cw-storage-plus/containers/_meta.json +++ b/src/pages/cw-storage-plus/containers/_meta.json @@ -1,5 +1,6 @@ { "item": "Item", "map": "Map", - "deque": "Deque" + "deque": "Deque", + "indexed-map": "IndexedMap" } diff --git a/src/pages/cw-storage-plus/containers/indexed-map.mdx b/src/pages/cw-storage-plus/containers/indexed-map.mdx new file mode 100644 index 00000000..f37c6301 --- /dev/null +++ b/src/pages/cw-storage-plus/containers/indexed-map.mdx @@ -0,0 +1,252 @@ +import { Callout } from "nextra/components"; + +# IndexedMap + +An +[`IndexedMap`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.IndexedMap.html) +is a map that, apart from the usual map key `K` has some secondary indexes `I` +that can be used to look up values. + + +There's no limit to how many indexes you can have, but be careful. Using many indexes can increase the complexity of +storage writes - with every write, the list of indexes is iterated over since +they might need to be updated. + + +As always, we encourage you to explore the +[`API reference`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.IndexedMap.html) +for a full list of available methods and more rigid definitions of the types +involved. + +## Case study: User lookup + +Imagine we have a list of users. Every user is uniquely identified by their +address, so it makes sense to use that as the primary key. We'll need to look up +users by their address - if Alice calls the contract, we should be able to look +up her user data by the address with which she called. + + +Every snippet on this page builds on the previous ones. Make sure you've + understood the previous snippet before moving on to the next one, and feel free to go back and forth as needed. + + +This is easy to model with a normal `Map`. + +```rust template="storage" +use cw_storage_plus::Map; + +type Handle = String; + +#[cw_serde] +struct User { + handle: Handle, + country: String, +} + +let _users = Map::::new("u"); +``` + +Great! But what if we want to look up users by their handles? Our only real +option here is to iterate over all users and check if the handle matches. With a +big enough user base, this could be slow and expensive. + +### Setting up an `IndexedMap` with a `UniqueIndex` + +A better way would be to maintain a secondary index. Let's try to do this. + +```rust template="storage" +use cw_storage_plus::{index_list, IndexedMap, UniqueIndex}; + +use crate::users::*; + +#[index_list(User)] +struct UserIndexes<'a> { + handle_ix: UniqueIndex<'a, Handle, User, Addr>, +} + +let user_indexes = UserIndexes { + handle_ix: UniqueIndex::new(|user| user.handle.clone(), "uh"), +}; + +let _users = IndexedMap::::new("u", user_indexes); +``` + + +The `index_list` macro is used to define a list of indexes for a given struct. + This is a helper macro that generates an implementation of the + [`IndexList`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/trait.IndexList.html) trait, with all the fields of the struct as indexes. + + +Here's what happens step-by-step + +- We define a struct `UserIndexes` that holds all the indexes we want to use. In + this case, we only have one index - `handle`, indexing the `handle` field. +- We construct our `UserIndexes`. Notice the `UniqueIndex` constructor takes two + parameters: + - A function pointer (here we provide an anonymous function). This function is + supposed to take the value of an entry and produce the secondary key. + Without this, the `IndexedMap` would not know how to create the index. + - A prefix. The index needs its own storage namespace. Note the index prefix + has to be distinct from the `IndexedMap` prefix (or any other prefix used + with this contract's storage). +- We construct an `IndexedMap` with that list of indexes. + + + Note the `UniqueIndex` type has three type parameters. The order might be + slightly confusing, but here it goes: + +- The secondary key type (`Handle` in our case). +- The value type (`User` in our case). +- The primary key type (`Addr` in our case). + + + + +If you're using + [`UniqueIndex`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.UniqueIndex.html), + it's your responsibility to ensure that the index you've built has unique + keys. If you have two users with the same handle, the index will be + overwritten and only contain the last user. Be careful! + + If you need to store a key that is not unique, you'll want to use a [`MultiIndex`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.MultiIndex.html) - see the [next section](#lookup-by-country). + + + +Under the hood, the `IndexedMap` will store the data using a regular `Map` for +the primary key, and then another `Map` for each secondary index. This is how efficient lookups are achieved. + +Again, every update to this storage structure will have to update all the +indexes - that's the price we pay for efficient lookups. + + + +### Taking advantage of the secondary index + +As with a regular `Map`, you can +[load](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.IndexedMap.html#method.load) +data, +[save](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.IndexedMap.html#method.save) +data, +[remove](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/struct.IndexedMap.html#method.remove) +data, and iterate over the `IndexedMap`. + +```rust template="storage" +use cw_storage_plus::IndexedMap; + +use crate::users::*; + +let users = IndexedMap::::new("u", user_indexes("uh")); + +let alice_addr = Addr::unchecked("aaa"); +let alice_user = User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), +}; + +let bob_addr = Addr::unchecked("bbb"); +let bob_user = User { + handle: "bob".to_string(), + country: "Bikini Bottom".to_string(), +}; + +users + .save(&mut storage, alice_addr.clone(), &alice_user) + .unwrap(); + +users + .save(&mut storage, bob_addr.clone(), &bob_user) + .unwrap(); + +assert_eq!( + users.load(&storage, alice_addr.clone()).unwrap(), + alice_user +); + +assert_eq!( + users + .range(&storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(), + vec![ + (alice_addr.clone(), alice_user.clone()), + (bob_addr.clone(), bob_user.clone()), + ] +); +``` + +But now we can also look up users by their handle. + +```rust template="storage" +use cw_storage_plus::IndexedMap; + +use crate::users::*; + +let (_key, alice) = users + .idx + .handle_ix // access the `handle_ix` index + .item(&storage, "alice".to_string()) // load a specific record + .unwrap() + .unwrap(); + +assert_eq!( + alice, + User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + } +); +``` + +We can also iterate over records using bounds based on the secondary index. +Here's how we can find all user handles starting with "a" or "b". + +```rust template="storage" +use cw_storage_plus::{Bound, IndexedMap}; + +use crate::users::*; + +let found_users = users + .idx + .handle_ix + .range( + &storage, + Some(Bound::inclusive("a")), + Some(Bound::exclusive("c")), + Order::Ascending, + ) + .collect::>>() + .unwrap(); + +assert_eq!(found_users.len(), 2); +assert_eq!( + found_users[0], + ( + Addr::unchecked("aaa"), + User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + } + ) +); +assert_eq!(found_users[1].1.handle, "bob"); +``` + +If we're only interested in the addresses (primary keys in our case), we can use +the `keys` method. + +```rust template="storage" +use cw_storage_plus::{Bound, IndexedMap}; + +use crate::users::*; + +let found_keys = users + .idx + .handle_ix + .keys(&storage, Some(Bound::inclusive("b")), None, Order::Ascending) + .collect::>>() + .unwrap(); + +assert_eq!(found_keys.len(), 2); +assert_eq!(found_keys[0], Addr::unchecked("bbb")); +assert_eq!(found_keys[1], Addr::unchecked("ccc")); +```