Indexes
Indexes play a vital role in facilitating the process of iterating through primary keys while providing valuable information. Let's delve into a model that involves multiple tokens within a system, with each token being uniquely owned. In this context, it is essential to establish an association between an owner and a token, enabling seamless querying of tokens based on their respective owners.
struct Token {
pub owner: Addr,
pub ticker: String
}Tokens are distinguishable through an auto-incremented key, serving as the primary identifier, guaranteeing the uniqueness of each token.
(TokenPK) -> TokenThe owner's index would have the following structure:
(owner, TokenPK) -> TokenTokenPK is a reference to Token data, and the owner:TokenPK key is a reference to a specific Token. By executing two database queries, one can access the Token data. To obtain all the tokens associated with a specific owner, a prefix range query, as depicted above, can be employed.
pub const TOKENS: Map<U8Key, Token> = Map::new("tokens");
// (owner Address, Token PK) -> u8 key
pub const OWNER_INDEX: Map<(&Addr, U8Key), &[u8]> = Map::new("owner_tokenpk");
Utilizing the owner information as the key streamlines token access. Nonetheless, it's important to note that any alterations to the state of TOKENS necessitate corresponding updates to the owner field.
This concept aligns with storage-plus indexing.
The previously mentioned solution works, but its efficiency is compromised by the extensive code complexity. This is where storage-plus/IndexedMap comes into the picture. IndexedMap is a storage manager that incorporates built-in indexing capabilities.
It encompasses two types of indexes: Unique Indexes and Multi Indexes.
Unique Indexes
Maintaining the exclusivity of a data field in a database is a typical necessity. UniqueIndex is an indexing utility that aids in accomplishing this functionality.
Let's go over the code step by step:
Within the attributes of a Token, the identifier stands out as a distinct and unique value:
TokenIndexes is a struct for defining indexes of Token struct:
IndexList is an interface for building the indexes:
The above code is an index builder function. It builds composite keys with the given function, and accepts a key to identify the index bucket.
See test code below:
The last line will crash with an error:
Multi indexes
Multi indexes are used when the structure is indexed by non-unique values. Here is a case from the cw721 smart contract:
We observe that the owner index is a MultiIndex, which permits duplicate keys. This is why the primary key is appended as the final element in the multi-index key. As the name suggests, this is an index organized by owner, enabling us to manage and iterate through all the tokens associated with a particular owner.
It's essential to emphasize that the key, along with its components in the case of a composite key, must adhere to the PrimaryKey trait. In this instance, both the 2-tuple (_, _) and Vec are compliant with the PrimaryKey requirement.
During index creation, we must supply an index function per index.
The index function plays a crucial role in generating the index key from the value and the primary key (always in Vec format) of the original map. Naturally, this necessitates the presence of the essential elements required for constructing the index key within the value.
In addition to the index function, it is essential to specify both the primary key's namespace and the namespace for the new index.
In this context, it's essential that the primary key's namespace corresponds to the one utilized during the creation of the index. We supply our TokenIndexes, specified as an IndexList-type parameter, as the second argument. This effectively links the underlying Map with the primary key and the defined indexes.
IndexedMap (along with the other Indexed* variants) essentially acts as an enhanced wrapper for the Map, offering a range of index functions and designated namespaces for establishing indexes over the original Map data. Moreover, it takes care of executing these index functions seamlessly during data storage, modification, or removal, allowing you to work with indexed data without having to be concerned with implementation intricacies.
Here is an example of how to use indexes in code:
Composite Multi Indexing
Let's delve into the following scenario: we have multiple batches stored based on their numeric batch ID, and these IDs can change. Furthermore, these batches need to be automatically promoted after any modifications. Our goal is to process all the pending batches with a status ranging from "Pending" to "Promoted," depending on various interactions with them. Additionally, each batch comes with an associated expiration time. We are primarily interested in those pending batches that have already expired, as these can be promoted. To accomplish this, we can create an index for the batches, using a composite key that incorporates both the batch status and its expiration timestamp. This composite key allows us to exclude both the already promoted batches and the pending ones that haven't reached their expiration.
To achieve this, we'll construct the index, generate the composite key, and iterate through all the pending batches with an expiration timestamp earlier than the current time.
Here's an example illustrating how to accomplish this using the Batch struct:
This example is similar to the previous one, above. The only differences are:
The composite key now has three elements: the batch status, the expiration timestamp, and the batch id (which is the primary key for the Batch data). We're using a U64Key for the batch id / pk. This is just for convenience. We could as well have used a plain Vec for it.
Now, here's how to use the indexed data:
A couple of important points to note:
joined_key() Function: The joined_key() function is utilized to generate the range key. This helpful function takes the (partial) composite key, which consists of the batch expiration timestamp and batch ID, and transforms it into a Vec with the correct format. This Vec is then used to establish a range bound.
sub_prefix() Function: The sub_prefix() function is employed to adjust the initial element of the composite key, which corresponds to the batch status. This adjustment is necessary because using prefix() in this context, where we have a 3-tuple, would mean fixing the first two elements of the key, which isn't required for our purpose.
The iteration begins from a None state and progresses to the bound key formed from the current timestamp. This approach allows us to effectively list only the pending batches that have already expired but remain unprocessed.
That's the gist of it. Following this process, we can iterate through the results and carry out actions such as changing their status from "Pending" to "Promoted," or any other necessary tasks.
Last updated
Was this helpful?