This guide will help you get started with the ic-dbms canister. Follow the steps below to set up your development environment and deploy the canister on the Internet Computer.
I strongly suggest you to setup a Cargo workspace including two crates:
ic-dbms-canister crate.Also, it is required to have the following config.toml at .cargo/config.toml to bypass the issue with get-random, which is required for the uuid crate:
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="custom"']
First, create a new Rust library crate for your database schema with the following dependencies in your Cargo.toml:
[dependencies]
candid = "0.10"
ic-dbms-api = "0.1"
serde = "1"
Then inside of lib.rs, define your database schema using Rust structs deriving CandidType, Deserialize, Table and Clone. For example:
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
pub name: Text,
pub email: Text,
}
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "posts"]
pub struct Post {
#[primary_key]
pub id: Uint32,
pub title: Text,
pub content: Text,
#[foreign_key(entity = "User", table = "users", column = "id")]
pub user: Uint32,
}
Mind that you have to follow the following rules when defining your schema:
#[primary_key].#[foreign_key(entity = "...", table = "...", column = "...")].BlobBooleanDateDateTimeDecimalInt32Int64Nullable<Type>PrincipalTextUint32Uint64UuidAnd that’s it for the schema crate! This for each table you want to define in your database will create also the following types:
{struct_name}Record{struct_name}InsertRequest{struct_name}UpdateRequest{struct_name}ForeignFetcher (used only by the ic-dbms-canister internally)In order to setup the DBMS canister, you need to create a new Rust project and add the following dependencies to your Cargo.toml:
[dependencies]
candid = "0.10"
ic-cdk = "0.19"
ic-dbms-api = "0.1"
ic-dbms-canister = "0.1"
serde = "1"
Then inside your lib.rs, you must setup the schema by just doing the following:
use ic_dbms_canister::prelude::DbmsCanister;
#[derive(DbmsCanister)]
#[tables(User = "users", Post = "posts")]
pub struct IcDbmsCanisterGenerator;
ic_cdk::export_candid!();
The canister API will be automatically generated based on the defined tables, with the following methods:
service : (IcDbmsCanisterArgs) -> {
acl_add_principal : (principal) -> (Result);
acl_allowed_principals : () -> (vec principal) query;
acl_remove_principal : (principal) -> (Result);
begin_transaction : () -> (nat);
commit : (nat) -> (Result);
delete_posts : (DeleteBehavior, opt Filter_1, opt nat) -> (Result_1);
delete_users : (DeleteBehavior, opt Filter_1, opt nat) -> (Result_1);
insert_posts : (PostInsertRequest, opt nat) -> (Result);
insert_users : (UserInsertRequest, opt nat) -> (Result);
rollback : (nat) -> (Result);
select_posts : (Query, opt nat) -> (Result_2) query;
select_users : (Query_1, opt nat) -> (Result_3) query;
update_posts : (PostUpdateRequest, opt nat) -> (Result_1);
update_users : (UserUpdateRequest, opt nat) -> (Result_1);
}
This is enough to setup the canister with the tables defined in the schema crate.
[!NOTE] If you want you can add custom logic inside of the canister and export additional methods with the
ic_cdkmacros. Mind that at the moment it is not possible to add more logic to theinitmethod of the canister. Anyway I honestly suggest you to keep the canister as simple as possible and just use it as a database canister. If you want to add more complex logic, you can create another canister which interacts with the database canister.
At this point you can just build the canister with:
mkdir -p "${WASM_DIR}"
echo "Building ${canister_name} Canister"
cargo build --target wasm32-unknown-unknown --release --package "${canister_name}"
ic-wasm "target/wasm32-unknown-unknown/release/${canister_name}.wasm" -o "${WASM_DIR}/${wasm_name}.wasm" shrink
candid-extractor "${WASM_DIR}/${wasm_name}.wasm" > "${WASM_DIR}/${wasm_name}.did"
gzip -k "${WASM_DIR}/${wasm_name}.wasm" --force
The canister has currently the following init arguments:
type IcDbmsCanisterArgs = variant { Upgrade; Init : IcDbmsCanisterInitArgs };
type IcDbmsCanisterInitArgs = record { allowed_principals : vec principal };
So you must provide a Init variant of IcDbmsCanisterArgs with a list of allowed_principals which will be able to interact with the canister.
[!WARNING] Mind that only principals in this list will be able to interact with the canister, so make sure to include all the necessary principals!
In order to interact with the canister, you can use the ic-dbms-client crate which provides a high-level API to interact with the canister.
You first need to add the following dependency to your Cargo.toml:
[dependencies]
ic-dbms-api = "0.1"
ic-dbms-client = "0.1"
Then you can create a client instance and use it to interact with the canister:
use ic_dbms_client::{IcDbmsCanisterClient, Client as _};
let principal = Principal::from_text("mxzaz-hqaaa-aaaar-qaada-cai")?;
let client = IcDbmsCanisterClient::new(principal);
// insert a new user
let alice = UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
email: "alice@example.com".into(),
age: Nullable::Value(30.into()),
};
client
.insert::<User>(User::table_name(), alice, None)
.await??;
// select users
let query: Query<User> = Query::builder().all().build();
let users = client
.select::<User>(User::table_name(), query, None)
.await??;
for user in users {
println!(
"User: id={:?}, name={:?}, email={:?}, age={:?}",
user.id, user.name, user.email, user.age
);
}
If you need to add queries in integration tests and you use pocket-ic, you can add ic-dbms-client with the pocket-ic feature enabled:
[dependencies]
ic-dbms-client = { version = "0.1", features = ["pocket-ic"] }
Then inside your integration tests you can create a client instance using the PocketIcAgent:
use ic_dbms_client::prelude::{Client as _, IcDbmsPocketIcClient};
let client = IcDbmsPocketIcClient::new(canister_principal, admin_principal, &pic);
let insert_request = UserInsertRequest {
id: Uint32::from(1),
name: "Alice".into(),
email: "alice@example.com".into(),
};
client
.insert::<User>(User::table_name(), insert_request, None)
.await
.expect("failed to call canister")
.expect("failed to insert user");