Skip to content

Commit f51b370

Browse files
authored
chore: migrate motoko/composite_query to icp-cli (#1382)
1 parent 09dca8c commit f51b370

9 files changed

Lines changed: 116 additions & 119 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: composite_query
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
paths:
8+
- motoko/composite_query/**
9+
- .github/workflows/composite_query.yml
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
motoko-composite_query:
17+
runs-on: ubuntu-24.04
18+
container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2
19+
env:
20+
ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
steps:
22+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23+
- name: Deploy and test
24+
working-directory: motoko/composite_query
25+
run: |
26+
icp network start -d
27+
icp deploy --cycles 30t
28+
make test

.github/workflows/motoko-composite-query-example.yaml

Lines changed: 0 additions & 41 deletions
This file was deleted.

motoko/composite_query/Makefile

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
1-
.PHONY: all
2-
all: build
1+
.PHONY: test topup
32

4-
.PHONY: build
5-
.SILENT: build
6-
build:
7-
dfx canister create --all --no-wallet
8-
dfx build
3+
topup:
4+
icp canister top-up --amount 30t backend
95

10-
.PHONY: install
11-
.SILENT: install
12-
install: build
13-
dfx canister install --all
6+
test:
7+
@echo "=== Test 1: put inserts a key-value pair ==="
8+
@icp canister call backend put '(0, "zero")' && \
9+
echo "PASS" || (echo "FAIL" && exit 1)
1410

15-
.PHONY: upgrade
16-
.SILENT: upgrade
17-
upgrade: build
18-
dfx canister install --all --mode=upgrade
11+
@echo "=== Test 2: get (composite query) retrieves the stored value ==="
12+
@# This exercises the composite query: Map.get calls Bucket.get on a child canister.
13+
@result=$$(icp canister call --query backend get '(0)') && \
14+
echo "$$result" && \
15+
echo "$$result" | grep -q 'opt "zero"' && \
16+
echo "PASS" || (echo "FAIL" && exit 1)
1917

20-
.PHONY: test
21-
.SILENT: test
22-
test: install
23-
dfx canister call Map test '()'
24-
dfx canister call --query Map get '(15)' \
25-
| grep '(opt "15")' && echo 'PASS'
18+
@echo "=== Test 3: getUpdate returns the same value via an update call ==="
19+
@result=$$(icp canister call backend getUpdate '(0)') && \
20+
echo "$$result" && \
21+
echo "$$result" | grep -q 'opt "zero"' && \
22+
echo "PASS" || (echo "FAIL" && exit 1)
2623

27-
.PHONY: clean
28-
.SILENT: clean
29-
clean:
30-
rm -fr .dfx
24+
@echo "=== Test 4: get returns null for a missing key ==="
25+
@result=$$(icp canister call --query backend get '(99)') && \
26+
echo "$$result" && \
27+
echo "$$result" | grep -q 'null' && \
28+
echo "PASS" || (echo "FAIL" && exit 1)
29+
30+
@echo "=== Test 5: composite query routes correctly across all four buckets ==="
31+
@# Keys 0-3 each map to a different bucket (k % 4): 0→B0, 1→B1, 2→B2, 3→B3.
32+
@# The composite query must call the right child canister for each key.
33+
@icp canister call backend put '(1, "one")' && \
34+
icp canister call backend put '(2, "two")' && \
35+
icp canister call backend put '(3, "three")' && \
36+
result0=$$(icp canister call --query backend get '(0)') && \
37+
result1=$$(icp canister call --query backend get '(1)') && \
38+
result2=$$(icp canister call --query backend get '(2)') && \
39+
result3=$$(icp canister call --query backend get '(3)') && \
40+
echo "$$result0" && echo "$$result1" && echo "$$result2" && echo "$$result3" && \
41+
echo "$$result0" | grep -q 'opt "zero"' && \
42+
echo "$$result1" | grep -q 'opt "one"' && \
43+
echo "$$result2" | grep -q 'opt "two"' && \
44+
echo "$$result3" | grep -q 'opt "three"' && \
45+
echo "PASS" || (echo "FAIL" && exit 1)

motoko/composite_query/README.md

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,54 @@
11
# Composite queries
22

3-
This example modifies the simple actor class example to demonstrate the implementation of composite queries.
3+
On the Internet Computer, regular query functions are fast (no consensus) but have one strict limitation: **they cannot call other canisters**. Composite queries lift this restriction — a `composite query func` can call query methods on other canisters while keeping the speed benefit of a query call.
44

5-
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.
5+
For more background see [Composite queries](https://docs.internetcomputer.org/guides/canister-calls/parallel-inter-canister-calls/#composite-queries) in the ICP developer docs.
66

7-
In the original example, shared functions `Map.get` and `Bucket.get` were both implemented as
8-
update methods so that `Map.get` could call `Bucket.get`.
7+
This example implements a distributed key-value store (`Map`) that shards its entries across four dynamically-installed `Bucket` child canisters. Looking up a key requires calling the appropriate bucket:
98

10-
In this version `Bucket.get` is implemented as a query function and `Map.get` as a composite query function.
11-
Although queries and composite queries are fast, composite queries can only be invoked as ingress messages, either
12-
using `dfx` (see below) or an agent through, for example, a browser front-end (not illustrated here).
9+
- `get(k)`**composite query**: delegates to the correct `Bucket.get(k)` as a cross-canister query call. Fast, no consensus.
10+
- `getUpdate(k)`**update call**: same lookup, but via an update call to the bucket. Slower (goes through consensus) but provided here for comparison.
1311

14-
In detail, the example provides actor `Map`.
15-
`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.
12+
Both functions return the same result; the difference is latency and call semantics.
1613

17-
[Map.mo](./src/map/Map.mo) imports a Motoko _actor class_ `Bucket(i, n)`
18-
from library [Buckets.mo](./src/map/Buckets.mo).
19-
It also imports `mo:core/Cycles` to share its cycles amongst the buckets it creates.
14+
## Architecture
2015

21-
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`).
22-
23-
Each asynchronous instantiation of the `Bucket` actor class corresponds to the dynamic, programmatic installation of a new `Bucket` canister.
24-
25-
Each new `Bucket` must be provisioned with enough cycles to pay for its installation and running costs.
26-
`Map` achieves this by attaching an equal share of `Map`'s initial cycle balance to each asynchronous call to `Bucket(n, i)`, using the syntax `await (with cycles = cycleShare) Buckets.Bucket(n, i)`.
27-
28-
`Map`'s `test` method simply `put`s 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.
16+
```
17+
Map (backend)
18+
n = 4 buckets ┌── Bucket 0 (keys 0, 4, 8, …)
19+
key % n routes ───┼── Bucket 1 (keys 1, 5, 9, …)
20+
├── Bucket 2 (keys 2, 6, 10, …)
21+
└── Bucket 3 (keys 3, 7, 11, …)
22+
```
2923

30-
## Deploying from ICP Ninja
24+
`Map.put(k, v)` dynamically installs a `Bucket` if one does not exist for `k % 4`, then stores the entry there. `Map.get(k)` and `Map.getUpdate(k)` both route to the same bucket via `k % 4`.
3125

32-
[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/motoko/composite_query)
26+
## Build and deploy from the command line
3327

34-
## Build and deploy from the command-line
28+
### Prerequisites
3529

36-
### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install)
30+
- Node.js
31+
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`
32+
- ic-mops: `npm install -g ic-mops`
3733

38-
### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/)
34+
### Install
3935

40-
### 3. Navigate into the project's directory.
36+
```bash
37+
git clone https://github.com/dfinity/examples
38+
cd examples/motoko/composite_query
39+
```
4140

42-
### 4. Deploy the project to your local environment:
41+
### Deploy and test
4342

43+
```bash
44+
icp network start -d
45+
icp deploy --cycles 30t
46+
make test
47+
icp network stop
4448
```
45-
dfx start --background --clean && dfx deploy
46-
```
49+
50+
> `icp deploy --cycles 30t` is required because `Map` dynamically creates `Bucket` canisters — it needs extra cycles to fund their installation. If tests fail with an out-of-cycles error, run `make topup`.
4751
4852
## Security considerations and best practices
4953

50-
If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices.
54+
Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import VarArray "mo:core/VarArray";
33
import Cycles "mo:core/Cycles";
44
import Buckets "Buckets";
55

6-
persistent actor Map {
6+
actor Map {
77

88
let n = 4; // number of buckets
99

motoko/composite_query/dfx.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

motoko/composite_query/icp.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
canisters:
2+
- name: backend
3+
recipe:
4+
type: "@dfinity/motoko@v5.0.0"

motoko/composite_query/mops.toml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# Motoko dependencies (https://mops.one/)
2-
31
[toolchain]
4-
moc = "1.5.1"
2+
moc = "1.9.0"
53

64
[dependencies]
7-
core = "2.4.0"
5+
core = "2.5.0"
86

97
[moc]
10-
# M0236: use context dot notation (e.g. map.get(k) instead of Map.get(map, compare, k))
11-
# M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically)
12-
# M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate<T>)
13-
args = ["-W=M0236,M0237,M0223"]
8+
# M0236: use context dot notation
9+
# M0237: redundant explicit implicit arguments
10+
# M0223: redundant type instantiation
11+
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]
12+
13+
[canisters.backend]
14+
main = "backend/app.mo"

0 commit comments

Comments
 (0)