Get Started
Get Started
- Prerequisites
- Project Setup
- Define Your Schema
- Create the DBMS Canister
- Deploy the Canister
- Quick Example: Complete Workflow
- Integration Testing
- Next Steps
This guide walks you through setting up a complete database canister using ic-dbms. By the end, you’ll have a working canister with CRUD operations, transactions, and access control.
Prerequisites
Before starting, ensure you have:
- Rust 1.85.1 or later
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown- dfx (Internet Computer SDK)
ic-wasm:cargo install ic-wasmcandid-extractor:cargo install candid-extractor
Project Setup
Workspace Structure
We recommend organizing your project as a Cargo workspace with two crates:
my-dbms-project/
├── Cargo.toml # Workspace manifest
├── schema/ # Schema definitions (reusable types)
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
└── canister/ # The DBMS canister
├── Cargo.toml
└── src/
└── lib.rs
Workspace Cargo.toml:
[workspace]
members = ["schema", "canister"]
resolver = "2"
Cargo Configuration
Create .cargo/config.toml to configure the getrandom crate for WebAssembly:
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="custom"']
This is required because the uuid crate depends on getrandom.
Define Your Schema
Create the Schema Crate
Create schema/Cargo.toml:
[package]
name = "my-schema"
version = "0.1.0"
edition = "2024"
[dependencies]
candid = "0.10"
ic-dbms-api = "0.4"
serde = "1"
Define Tables
In schema/src/lib.rs, define your database tables using the Table derive macro:
use candid::{CandidType, Deserialize};
use ic_dbms_api::prelude::*;
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
#[sanitizer(TrimSanitizer)]
#[validate(MaxStrlenValidator(100))]
pub name: Text,
#[validate(EmailValidator)]
pub email: Text,
pub created_at: DateTime,
}
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "posts"]
pub struct Post {
#[primary_key]
pub id: Uint32,
#[validate(MaxStrlenValidator(200))]
pub title: Text,
pub content: Text,
pub published: Boolean,
#[foreign_key(entity = "User", table = "users", column = "id")]
pub author_id: Uint32,
}
Required derives: Table, CandidType, Deserialize, Clone
The Table macro generates additional types for each table:
| Generated Type | Purpose |
|---|---|
UserRecord |
Full record returned from queries |
UserInsertRequest |
Request type for inserting records |
UserUpdateRequest |
Request type for updating records |
UserForeignFetcher |
Internal type for relationship loading |
Create the DBMS Canister
Canister Dependencies
Create canister/Cargo.toml:
[package]
name = "my-canister"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
candid = "0.10"
ic-cdk = "0.19"
ic-dbms-api = "0.4"
ic-dbms-canister = "0.4"
my-schema = { path = "../schema" }
serde = "1"
Generate the Canister API
In canister/src/lib.rs:
use ic_dbms_canister::prelude::DbmsCanister;
use my_schema::{User, Post};
#[derive(DbmsCanister)]
#[tables(User = "users", Post = "posts")]
pub struct MyDbmsCanister;
ic_cdk::export_candid!();
The DbmsCanister macro generates a complete canister API:
service : (IcDbmsCanisterArgs) -> {
// ACL Management
acl_add_principal : (principal) -> (Result);
acl_allowed_principals : () -> (vec principal) query;
acl_remove_principal : (principal) -> (Result);
// Transactions
begin_transaction : () -> (nat);
commit : (nat) -> (Result);
rollback : (nat) -> (Result);
// Users CRUD
insert_users : (UserInsertRequest, opt nat) -> (Result);
select_users : (Query, opt nat) -> (Result_1) query;
update_users : (UserUpdateRequest, opt nat) -> (Result_2);
delete_users : (DeleteBehavior, opt Filter, opt nat) -> (Result_2);
// Posts CRUD
insert_posts : (PostInsertRequest, opt nat) -> (Result);
select_posts : (Query, opt nat) -> (Result_3) query;
update_posts : (PostUpdateRequest, opt nat) -> (Result_2);
delete_posts : (DeleteBehavior, opt Filter, opt nat) -> (Result_2);
}
Build the Canister
Create a build script or use the following commands:
# Build the canister
cargo build --target wasm32-unknown-unknown --release -p my-canister
# Optimize the WASM
ic-wasm target/wasm32-unknown-unknown/release/my_canister.wasm \
-o my_canister.wasm shrink
# Extract Candid interface
candid-extractor my_canister.wasm > my_canister.did
# Optionally compress
gzip -k my_canister.wasm --force
Deploy the Canister
Canister Init Arguments
The canister requires initialization arguments specifying which principals can access the database:
type IcDbmsCanisterArgs = variant {
Init : IcDbmsCanisterInitArgs;
Upgrade;
};
type IcDbmsCanisterInitArgs = record {
allowed_principals : vec principal;
};
Warning: Only principals in
allowed_principalscan perform database operations. Make sure to include all necessary principals (your frontend canister, admin principal, etc.).
Deploy with dfx
Create dfx.json:
{
"canisters": {
"my_dbms": {
"type": "custom",
"candid": "my_canister.did",
"wasm": "my_canister.wasm",
"build": []
}
}
}
Deploy:
dfx deploy my_dbms --argument '(variant { Init = record { allowed_principals = vec { principal "your-principal-here" } } })'
Quick Example: Complete Workflow
Here’s a complete example showing insert, query, update, and delete operations:
use ic_dbms_client::{IcDbmsCanisterClient, Client as _};
use my_schema::{User, UserInsertRequest, UserUpdateRequest};
use ic_dbms_api::prelude::*;
async fn example(canister_id: Principal) -> Result<(), Box<dyn std::error::Error>> {
let client = IcDbmsCanisterClient::new(canister_id);
// 1. INSERT a new user
let insert_req = UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
email: "alice@example.com".into(),
created_at: DateTime::now(),
};
client.insert::<User>(User::table_name(), insert_req, None).await??;
// 2. SELECT users
let query = Query::builder()
.filter(Filter::eq("name", Value::Text("Alice".into())))
.build();
let users = client.select::<User>(User::table_name(), query, None).await??;
println!("Found {} user(s)", users.len());
// 3. UPDATE the user
let update_req = UserUpdateRequest::builder()
.set_email("alice.new@example.com".into())
.filter(Filter::eq("id", Value::Uint32(1.into())))
.build();
let updated = client.update::<User>(User::table_name(), update_req, None).await??;
println!("Updated {} record(s)", updated);
// 4. DELETE the user
let deleted = client.delete::<User>(
User::table_name(),
DeleteBehavior::Restrict,
Some(Filter::eq("id", Value::Uint32(1.into()))),
None
).await??;
println!("Deleted {} record(s)", deleted);
Ok(())
}
Integration Testing
For integration tests using PocketIC, add ic-dbms-client with the pocket-ic feature:
[dev-dependencies]
ic-dbms-client = { version = "0.4", features = ["pocket-ic"] }
pocket-ic = "9"
Example test:
use ic_dbms_client::prelude::{Client as _, IcDbmsPocketIcClient};
use my_schema::{User, UserInsertRequest};
use pocket_ic::PocketIc;
#[tokio::test]
async fn test_insert_and_select() {
let pic = PocketIc::new();
// ... setup canister ...
let client = IcDbmsPocketIcClient::new(canister_id, admin_principal, &pic);
let insert_req = UserInsertRequest {
id: 1.into(),
name: "Test User".into(),
email: "test@example.com".into(),
created_at: DateTime::now(),
};
client
.insert::<User>(User::table_name(), insert_req, None)
.await
.expect("call failed")
.expect("insert failed");
let query = Query::builder().all().build();
let users = client
.select::<User>(User::table_name(), query, None)
.await
.expect("call failed")
.expect("select failed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].name.as_str(), "Test User");
}
Next Steps
Now that you have a working canister, explore these topics:
- CRUD Operations - Detailed guide on all database operations
- Querying - Filters, ordering, pagination, and field selection
- Transactions - ACID transactions with commit/rollback
- Relationships - Foreign keys and eager loading
- Access Control - Managing the ACL
- Custom Data Types - Define your own data types (enums, structs)
- Schema Definition - Complete schema reference
- Data Types - All supported field types