Composite queries
Overview
The Internet Computer Protocol supports two types of messages: updates and queries. An update message is executed on all nodes and persists canister state changes. A query message discards state changes and typically executes on a single node. It is possible to execute a query message as an update. In such a case, the query still discards the state changes, but the execution happens on all nodes and the result of execution goes through consensus. This “query-as-update” execution mode is also known as replicated query.
An update can call other updates and queries. However a query cannot make any calls, which can hinder development of scalable decentralized applications, especially those that shard data across multiple canisters.
Composite queries solve this problem. You can add composite queries to your canister using the following annotations:
- Candid:
composite_query
- Motoko:
composite query
- Rust:
#[query(composite = true)]
Users and the client-side JavaScript code can invoke a composite query endpoint of a canister using the same query URL for existing regular queries. In contrast to regular queries, a composite query can call other composite and regular queries. Due to limitations of the current implementation, composite queries have two restrictions:
Query | Update | Composite query |
---|---|---|
Cannot call other queries or composite queries | Can call other updates and queries ; Cannot call composite queries | Can call other queries and composite queries |
Can be called as an update | Cannot be called as a query | Cannot be called as an update |
Can call canisters on another subnet | Can call canisters on another subnet | Cannot call canisters on another subnet |
Composite queries were enabled in the following releases:
Platform / Language | Version |
---|---|
Internet computer mainnet | Release 7742d96ddd30aa6b607c9d2d4093a7b714f5b25b |
Candid | 2023-06-30 (Rust 0.9.0) |
Motoko | 0.9.4, revision: 2d9902f |
Rust | 0.6.8 |
Sample code
As an example, consider a partitioned key-value store, where a single frontend does the following for a put
and get
call:
- First, determines the ID of the data partition canister that holds the value with the given key.
- Then, makes a call into the
get
orput
function of that canister and parses the result.
- Motoko
- Rust
- TypeScript
- Python
import Debug "mo:base/Debug";
import Array "mo:base/Array";
import Cycles "mo:base/ExperimentalCycles";
import Buckets "Buckets";
actor Map {
let n = 4; // number of buckets
// divide initial balance amongst self and buckets
let cycleShare = Cycles.balance() / (n + 1);
type Key = Nat;
type Value = Text;
type Bucket = Buckets.Bucket;
let buckets : [var ?Bucket] = Array.init(n, null);
public func getUpdate(k : Key) : async ?Value {
switch (buckets[k % n]) {
case null null;
case (?bucket) await bucket.get(k);
};
};
public composite query func get(k : Key) : async ?Value {
switch (buckets[k % n]) {
case null null;
case (?bucket) await bucket.get(k);
};
};
public func put(k : Key, v : Value) : async () {
let i = k % n;
let bucket = switch (buckets[i]) {
case null {
// provision next send, i.e. Bucket(n, i), with cycles
Cycles.add(cycleShare);
let b = await Buckets.Bucket(n, i); // dynamically install a new Bucket
buckets[i] := ?b;
b;
};
case (?bucket) bucket;
};
await bucket.put(k, v);
};
public func test() : async () {
var i = 0;
while (i < 16) {
let t = debug_show(i);
assert (null == (await getUpdate(i)));
Debug.print("putting: " # debug_show(i, t));
await Map.put(i, t);
assert (?t == (await getUpdate(i)));
i += 1;
};
};
};
use ic_cdk::api::call::{call};
use ic_cdk::api::management_canister::main::{CreateCanisterArgument, create_canister, InstallCodeArgument, install_code, CanisterInstallMode};
use ic_cdk::api::management_canister::provisional::CanisterSettings;
use ic_cdk_macros::{query, update};
use candid::Principal;
use std::sync::Arc;
use std::sync::RwLock;
const NUM_PARTITIONS: usize = 5;
// Inline wasm binary of data partition canister
pub const WASM: &[u8] =
include_bytes!("../../target/wasm32-unknown-unknown/release/data_partition.wasm");
thread_local! {
// A list of canister IDs for data partitions
static CANISTER_IDS: Arc<RwLock<Vec<Principal>>> = Arc::new(RwLock::new(vec![]));
}
#[update]
async fn put(key: u128, value: u128) -> Option<u128> {
// Create partitions if they don't exist yet
if CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids.len() == 0
}) {
for _ in 0..NUM_PARTITIONS {
create_data_partition_canister_from_wasm().await;
}
}
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Put in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "put", (key, value), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
#[query(composite = true)]
async fn get(key: u128) -> Option<u128> {
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Get in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "get", (key, ), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
#[update]
async fn get_update(key: u128) -> Option<u128> {
let canister_id = get_partition_for_key(key);
ic_cdk::println!("Get as update in frontend for key={} .. using backend={}", key, canister_id.to_text());
match call(canister_id, "get", (key, ), ).await {
Ok(r) => {
let (res,): (Option<u128>,) = r;
res
},
Err(_) => None,
}
}
fn get_partition_for_key(key: u128) -> Principal {
let canister_id = CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids[lookup(key).0 as usize]
});
canister_id
}
#[query(composite = true)]
fn lookup(key: u128) -> (u128, String) {
let r = key % NUM_PARTITIONS as u128;
(r, CANISTER_IDS.with(|canister_ids| {
let canister_ids = canister_ids.read().unwrap();
canister_ids[r as usize].to_text()
}))
}
async fn create_data_partition_canister_from_wasm() {
let create_args = CreateCanisterArgument {
settings: Some(CanisterSettings {
controllers: Some(vec![ic_cdk::id()]),
compute_allocation: Some(0.into()),
memory_allocation: Some(0.into()),
freezing_threshold: Some(0.into()),
})
};
let canister_record = create_canister(create_args).await.unwrap();
let canister_id = canister_record.0.canister_id;
ic_cdk::println!("Created canister {}", canister_id);
let install_args = InstallCodeArgument {
mode: CanisterInstallMode::Install,
canister_id,
wasm_module: WASM.to_vec(),
arg: vec![],
};
install_code(install_args).await.unwrap();
CANISTER_IDS.with(|canister_ids| {
let mut canister_ids = canister_ids.write().unwrap();
canister_ids.push(canister_id);
});
}
Resources
The following example canisters demonstrate how to use composite queries:
Feedback and suggestions can be contributed on the forum here: https://forum.dfinity.org/t/proposal-composite-queries/15979