Skip to main content

Composite queries

View this sample's code on GitHub

Overview

This example modifies the simple actor class example to demonstrate the implementation of composite queries.

The original example demonstrates a simple use of actor classes, allowing a program to dynamically install new actors (that is, canisters). It also demonstrates a multi-canister project, and actors using inter-actor communication through shared functions.

In the original example, shared functions Map.get and Bucket.get were both implemented as update methods so that Map.get could call Bucket.get.

In this version Bucket.get is implemented as a query function and Map.get as a composite query function. Although queries and composite queries are fast, composite queries can only be invoked as ingress messages, either using dfx (see below) or an agent through, for example, a browser front-end (not illustrated here).

In detail, the example provides actor Map. Map is a dead-simple, distributed key-value store, mapping Nat to Text values, with entries stored in a small number of separate Bucket actors, installed on demand.

Map.mo imports a Motoko actor class Bucket(i, n) from library Buckets.mo. It also imports the ExperimentalCycles base library, naming it Cycles for short, to share its cycles amongst the buckets it creates.

Each call to Buckets.Bucket(n, i) within Map instantiates a new Bucket instance (the i-th of n) dedicated to those entries of the Map whose key hashes to i (by taking the remainder of the key modulo division by n).

Each asynchronous instantiation of the Bucket actor class corresponds to the dynamic, programmatic installation of a new Bucket canister.

Each new Bucket must be provisioned with enough cycles to pay for its installation and running costs. Map achieves this by adding an equal share of Map's initial cycle balance to each asynchronous call to Bucket(n, i), using a call to Cycles.add(cycleShare).

Map's test method simply puts 16 consecutive entries into Map. These entries are distributed evenly amongst the buckets making up the key-value store. Adding the first entry to a bucket takes longer than adding a subsequent one, since the bucket needs to be installed on first use.

Prerequisites

Verify the following before running this demo:

  • You have downloaded and installed dfx.

  • You have stopped any process that would create a port conflict on 8000.

  • Clone the example dapp project: git clone https://github.com/dfinity/examples

Install

Step 1: Start a local internet computer.

dfx start

Step 2: Open a new terminal window.

Step 3: Deploy the Map canister:

cd examples/motoko/composite_query
dfx deploy

Step 4: Invoke the test method of canister Map to add some entries:

dfx canister call Map test '()'

Step 5: Observe the following result.

debug.print: putting: (0, "0")
debug.print: putting: (1, "1")
debug.print: putting: (2, "2")
debug.print: putting: (3, "3")
debug.print: putting: (4, "4")
debug.print: putting: (5, "5")
debug.print: putting: (6, "6")
debug.print: putting: (7, "7")
debug.print: putting: (8, "8")
debug.print: putting: (9, "9")
debug.print: putting: (10, "10")
debug.print: putting: (11, "11")
debug.print: putting: (12, "12")
debug.print: putting: (13, "13")
debug.print: putting: (14, "14")
debug.print: putting: (15, "15")
()

Step 6: Invoke the get composite query method of canister Main:

dfx canister call --query Map get '(15)'

Step 7: Observe the following result:

(opt "15")

Resources

Security considerations and best practices

If you base your application on this example, we recommend you familiarize yourself with and adhere to the security best practices for developing on ICP. This example may not implement all the best practices.