Calimero Collections Reference
This comprehensive reference covers all available collections in the Calimero SDK, their performance characteristics, and best practices for using them effectively.
Table of Contents
- Overview
- Core Collections
- Specialized Collections
- Performance Characteristics
- Collection Patterns
- Best Practices
- Migration from Standard Collections
Overview
Calimero collections are specifically designed for WebAssembly and decentralized applications. They provide:
- Wasm Optimization: Designed for efficient serialization and memory usage
- Persistent Storage: Automatic integration with Calimero's storage system
- Type Safety: Full Rust type safety with compile-time guarantees
- Performance: Optimized for common operations in decentralized apps
- Error Handling: All operations return
Result<T, StoreError>
for robust error handling
Core Collections
UnorderedMap
A hash map implementation optimized for Wasm with O(1) average case operations.
All operations return Result<T, StoreError>
.
Basic Usage
use calimero_storage::collections::UnorderedMap;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct UserRegistry {
users: UnorderedMap<String, UserProfile>,
user_count: u32,
}
#[app::logic]
impl UserRegistry {
pub fn add_user(&mut self, username: String, profile: UserProfile) -> app::Result<()> {
if self.users.contains_key(&username)? {
return app::bail!("User already exists");
}
self.users.insert(username, profile)?;
self.user_count += 1;
Ok(())
}
pub fn get_user(&self, username: &str) -> app::Result<Option<UserProfile>> {
self.users.get(username).map_err(Into::into)
}
pub fn update_user(&mut self, username: String, profile: UserProfile) -> app::Result<()> {
if !self.users.contains_key(&username)? {
return app::bail!("User not found");
}
self.users.insert(username, profile)?;
Ok(())
}
pub fn remove_user(&mut self, username: &str) -> app::Result<Option<UserProfile>> {
let user = self.users.remove(username)?;
if user.is_some() {
self.user_count -= 1;
}
Ok(user)
}
}
Advanced Operations
#[app::logic]
impl UserRegistry {
// Batch operations
pub fn add_multiple_users(&mut self, users: Vec<(String, UserProfile)>) -> app::Result<()> {
for (username, profile) in users {
if self.users.contains_key(&username)? {
return app::bail!("User {} already exists", username);
}
self.users.insert(username, profile)?;
self.user_count += 1;
}
Ok(())
}
// Iteration
pub fn get_all_users(&self) -> app::Result<Vec<UserProfile>> {
self.users.entries()?.collect()
}
// Search
pub fn search_users(&self, query: &str) -> app::Result<Vec<UserProfile>> {
Ok(self.users
.entries()?
.filter(|(username, _)| username.contains(query))
.map(|(_, profile)| profile)
.collect())
}
// Statistics
pub fn get_user_count(&self) -> app::Result<usize> {
self.users.len()
}
}
UnorderedSet
A hash set for unique elements with similar performance characteristics to UnorderedMap.
use calimero_storage::collections::UnorderedSet;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct AccessControl {
admins: UnorderedSet<String>,
moderators: UnorderedSet<String>,
}
#[app::logic]
impl AccessControl {
pub fn add_admin(&mut self, username: String) -> app::Result<()> {
self.admins.insert(username)?;
Ok(())
}
pub fn is_admin(&self, username: &str) -> app::Result<bool> {
self.admins.contains(username)
}
pub fn remove_admin(&mut self, username: &str) -> app::Result<bool> {
self.admins.remove(username)
}
pub fn get_admin_count(&self) -> app::Result<usize> {
self.admins.len()
}
pub fn get_all_admins(&self) -> app::Result<Vec<String>> {
self.admins.entries()?.collect()
}
}
Vector
A dynamic array implementation optimized for sequential access.
use calimero_storage::collections::Vector;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct TaskManager {
tasks: Vector<Task>,
completed_tasks: Vector<Task>,
}
#[app::logic]
impl TaskManager {
pub fn add_task(&mut self, task: Task) -> app::Result<()> {
self.tasks.insert(None, task)?;
Ok(())
}
pub fn complete_task(&mut self, index: u32) -> app::Result<()> {
if index >= self.tasks.len()? {
return app::bail!("Invalid task index");
}
let task = self.tasks.remove(index as usize)?;
self.completed_tasks.insert(None, task)?;
Ok(())
}
pub fn get_task(&self, index: u32) -> app::Result<Option<Task>> {
if index >= self.tasks.len()? {
return Ok(None);
}
self.tasks.get(index as usize)
}
pub fn get_all_tasks(&self) -> app::Result<Vec<Task>> {
self.tasks.entries()?.collect()
}
}
Specialized Collections
Note: Some of these specialized collections may be planned for future releases or available in different versions of the SDK. Check the latest SDK documentation for current availability.
LookupMap
A map optimized for cases where you need to iterate over keys or values frequently. This collection provides better iteration performance at the cost of slightly higher memory usage.
use calimero_storage::collections::LookupMap;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct Analytics {
// User -> Page views
page_views: LookupMap<String, u32>,
// Page -> Total views
total_views: LookupMap<String, u64>,
}
#[app::logic]
impl Analytics {
pub fn record_page_view(&mut self, user: String, page: String) -> app::Result<()> {
// Increment user's page views
let user_views = self.page_views.get(&user)?.unwrap_or(0);
self.page_views.insert(user, user_views + 1)?;
// Increment total page views
let total = self.total_views.get(&page)?.unwrap_or(0);
self.total_views.insert(page, total + 1)?;
Ok(())
}
pub fn get_user_stats(&self, user: &str) -> app::Result<u32> {
Ok(self.page_views.get(user)?.unwrap_or(0))
}
pub fn get_page_stats(&self, page: &str) -> app::Result<u64> {
Ok(self.total_views.get(page)?.unwrap_or(0))
}
pub fn get_top_pages(&self, limit: usize) -> app::Result<Vec<(String, u64)>> {
let mut pages: Vec<(String, u64)> = self.total_views
.entries()?
.map(|(page, views)| (page, views))
.collect();
pages.sort_by(|a, b| b.1.cmp(&a.1));
pages.truncate(limit);
Ok(pages)
}
}
TreeMap
A sorted map implementation for cases where you need ordered keys. This collection maintains keys in sorted order, enabling efficient range queries and ordered iteration.
use calimero_storage::collections::TreeMap;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct Leaderboard {
// Score -> List of users with that score
score_rankings: TreeMap<u32, UnorderedSet<String>>,
// User -> Current score
user_scores: UnorderedMap<String, u32>,
}
#[app::logic]
impl Leaderboard {
pub fn update_score(&mut self, user: String, new_score: u32) -> app::Result<()> {
// Remove user from old score ranking
if let Some(old_score) = self.user_scores.get(&user)? {
if let Some(users_at_score) = self.score_rankings.get(&old_score)? {
let mut updated_users = users_at_score.clone();
updated_users.remove(&user)?;
if updated_users.is_empty() {
self.score_rankings.remove(&old_score)?;
} else {
self.score_rankings.insert(old_score, updated_users)?;
}
}
}
// Add user to new score ranking
let mut users_at_score = self.score_rankings
.get(&new_score)?
.unwrap_or_else(|| UnorderedSet::new());
users_at_score.insert(user.clone())?;
self.score_rankings.insert(new_score, users_at_score)?;
// Update user's score
self.user_scores.insert(user, new_score)?;
Ok(())
}
pub fn get_top_players(&self, limit: usize) -> app::Result<Vec<(String, u32)>> {
let mut top_players = Vec::new();
// Iterate in reverse order (highest scores first)
for (score, users) in self.score_rankings.entries()?.rev() {
for user in users.entries()? {
top_players.push((user, score));
if top_players.len() >= limit {
break;
}
}
if top_players.len() >= limit {
break;
}
}
Ok(top_players)
}
pub fn get_user_rank(&self, user: &str) -> app::Result<Option<u32>> {
let user_score = match self.user_scores.get(user)? {
Some(score) => score,
None => return Ok(None),
};
let mut rank = 1;
// Count users with higher scores
for (score, users) in self.score_rankings.entries()?.rev() {
if score > user_score {
rank += users.len()? as u32;
} else {
break;
}
}
Ok(Some(rank))
}
}
When to Use Each Collection
Use UnorderedMap when:
- You need fast key-value lookups
- Order doesn't matter
- Memory efficiency is important
Use UnorderedSet when:
- You need to track unique elements
- Fast membership testing is required
- Order doesn't matter
Use Vector when:
- You need ordered elements
- Sequential access is common
- You frequently add/remove from the end
Use LookupMap when:
- You frequently iterate over keys or values
- Memory overhead is acceptable
- You need both map and iteration performance
Use TreeMap when:
- You need sorted keys
- Range queries are common
- Order matters for your use case
Performance Characteristics
Operation Complexity
Operation | UnorderedMap | UnorderedSet | Vector |
---|---|---|---|
Insert | O(1) avg | O(1) avg | O(1) |
Get | O(1) avg | O(1) avg | O(1) |
Remove | O(1) avg | O(1) avg | O(n) |
Iterate | O(n) | O(n) | O(n) |
Contains | O(1) avg | O(1) avg | O(n) |
Memory Usage
- UnorderedMap/UnorderedSet: Hash table overhead + key-value storage
- Vector: Dynamic array with growth factor
- All collections automatically handle serialization/deserialization
Collection Patterns
Entry API for Conditional Operations
#[app::logic]
impl UserRegistry {
pub fn get_or_create_user(&mut self, username: String) -> app::Result<&mut UserProfile> {
// This pattern doesn't work with current API - collections return Result
// Use conditional logic instead
if !self.users.contains_key(&username)? {
let default_profile = UserProfile::default();
self.users.insert(username.clone(), default_profile)?;
}
// Note: We can't return &mut due to Result wrapper
// Consider returning the value or using a different pattern
Ok(())
}
}
Batch Operations
#[app::logic]
impl UserRegistry {
pub fn bulk_update_users(&mut self, updates: Vec<(String, UserProfile)>) -> app::Result<()> {
for (username, profile) in updates {
self.users.insert(username, profile)?;
}
Ok(())
}
pub fn bulk_remove_users(&mut self, usernames: Vec<String>) -> app::Result<usize> {
let mut removed_count = 0;
for username in usernames {
if self.users.remove(&username)?.is_some() {
removed_count += 1;
}
}
Ok(removed_count)
}
}
Best Practices
Error Handling
Always handle the Result
returned by collection operations:
// ❌ Bad - ignoring errors
let value = self.users.get(&key).unwrap();
// ✅ Good - proper error handling
let value = match self.users.get(&key)? {
Some(v) => v,
None => return app::bail!("User not found"),
};
Memory Management
// ❌ Bad - creating unnecessary clones
let all_users: Vec<String> = self.users.entries()?
.map(|(k, _)| k.clone())
.collect();
// ✅ Good - avoid unnecessary cloning
let all_users: Vec<&String> = self.users.entries()?
.map(|(k, _)| k)
.collect();
Transaction Safety
#[app::logic]
impl UserRegistry {
pub fn transfer_user(&mut self, from: String, to: String) -> app::Result<()> {
// All operations are atomic - if any fail, all changes are rolled back
let profile = self.users.remove(&from)?
.ok_or_else(|| app::err!("Source user not found"))?;
self.users.insert(to, profile)?;
Ok(())
}
}
Migration from Standard Collections
Key Differences
Standard Collection | Calimero Collection | Key Changes |
---|---|---|
HashMap<K, V> | UnorderedMap<K, V> | Returns Result<T, StoreError> |
HashSet<T> | UnorderedSet<T> | Returns Result<T, StoreError> |
Vec<T> | Vector<T> | Returns Result<T, StoreError> |
Migration Example
// Before (standard collections)
use std::collections::HashMap;
struct OldApp {
users: HashMap<String, UserProfile>,
}
impl OldApp {
pub fn add_user(&mut self, username: String, profile: UserProfile) {
self.users.insert(username, profile);
}
}
// After (Calimero collections)
use calimero_storage::collections::UnorderedMap;
#[app::state]
#[derive(Default, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
struct NewApp {
users: UnorderedMap<String, UserProfile>,
}
#[app::logic]
impl NewApp {
pub fn add_user(&mut self, username: String, profile: UserProfile) -> app::Result<()> {
self.users.insert(username, profile)?;
Ok(())
}
}
Common Pitfalls
Forgetting Error Handling
// ❌ This will not compile
let user = self.users.get(&username).unwrap();
// ✅ Handle the Result
let user = self.users.get(&username)?;
Assuming Standard Collection Methods
// ❌ Standard collections don't have these methods
let count = self.users.len(); // Standard collections return usize directly
// ✅ Calimero collections return Result
let count = self.users.len()?; // Returns Result<usize, StoreError>
Ignoring Storage Errors
// ❌ Bad - storage errors can indicate serious issues
if let Ok(user) = self.users.get(&username) {
// Handle user
}
// ✅ Good - propagate errors up
let user = self.users.get(&username)?;
Next Steps
- Learn about Rust SDK Macros for application structure
- See Rust SDK Deep Dive for advanced patterns