Skip to content

Commit

Permalink
example: composite query demo (Rust + Motoko) (#587)
Browse files Browse the repository at this point in the history
This adds two example applications for the composite query feature, one in Rust and one in Motoko.
  • Loading branch information
crusso authored Jul 26, 2023
1 parent f85b099 commit 92eac86
Show file tree
Hide file tree
Showing 21 changed files with 653 additions and 4 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/motoko-composite-query-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: motoko-composite-query
on:
push:
branches:
- master
pull_request:
paths:
- motoko/composite_query/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/motoko-composite-query-example.yaml
- .github/workflows/motoko-composite-query-skip.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
motoko-composite-query-darwin:
runs-on: macos-12
steps:
- uses: actions/checkout@v1
- name: Provision Darwin
env:
DFX_VERSION: 0.15.0-beta.1
run: bash .github/workflows/provision-darwin.sh
- name: Motoko Composite Query Darwin
run: |
dfx start --background
pushd motoko/composite_query
make test
popd
motoko-composite-query-linux:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- name: Provision Linux
env:
DFX_VERSION: 0.15.0-beta.1
run: bash .github/workflows/provision-linux.sh
- name: Motoko Composite Query Linux
run: |
dfx start --background
pushd motoko/composite_query
make test
popd
17 changes: 17 additions & 0 deletions .github/workflows/motoko-composite-query-skip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: motoko-composite-query
on:
pull_request:
paths-ignore:
- motoko/composite_query/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/motoko-composite-query-example.yaml
- .github/workflows/motoko-composite-query-skip.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo 'Not needed - relevant folder not touched'
48 changes: 48 additions & 0 deletions .github/workflows/rust-composite_query-example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: rust-composite_query
on:
push:
branches:
- master
pull_request:
paths:
- rust/composite_query/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/rust-composite_query-example.yml
- .github/workflows/rust-composite_query-skip.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
rust-composite_query-darwin:
runs-on: macos-12
steps:
- uses: actions/checkout@v1
- name: Provision Darwin
run: bash .github/workflows/provision-darwin.sh
- name: Rust composite_query Darwin
run: |
dfx start --background
pushd rust/composite_query
dfx canister create kv_frontend
dfx build kv_frontend
dfx canister install kv_frontend
dfx canister call kv_frontend put '(1, 1337)'
dfx canister call kv_frontend get '(1)' | grep 1337
popd
rust-composite_query-linux:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- name: Provision Linux
run: bash .github/workflows/provision-linux.sh
- name: Rust composite_query Linux
run: |
dfx start --background
pushd rust/composite_query
dfx canister create kv_frontend
dfx build kv_frontend
dfx canister install kv_frontend
dfx canister call kv_frontend put '(1, 1337)'
dfx canister call kv_frontend get '(1)' | grep 1337
popd
17 changes: 17 additions & 0 deletions .github/workflows/rust-composite_query-skip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: rust-composite_query
on:
pull_request:
paths-ignore:
- rust/composite_query/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/rust-composite_query-example.yml
- .github/workflows/rust-composite_query-skip.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo 'Not needed - relevant folder not touched'
6 changes: 3 additions & 3 deletions motoko/classes/src/map/Buckets.mo
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ actor class Bucket(n : Nat, i : Nat) {
let map = Map.RBTree<Key, Value>(Nat.compare);

public func get(k : Key) : async ?Value {
assert((k % n) == i);
assert ((k % n) == i);
map.get(k);
};

public func put(k : Key, v : Value) : async () {
assert((k % n) == i);
map.put(k,v);
assert ((k % n) == i);
map.put(k, v);
};

};
2 changes: 1 addition & 1 deletion motoko/classes/src/map/Map.mo
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ actor Map {
let n = 4; // number of buckets

// divide initial balance amongst self and buckets
let cycleShare = Cycles.balance()/(n + 1);
let cycleShare = Cycles.balance() / (n + 1);

type Key = Nat;
type Value = Text;
Expand Down
30 changes: 30 additions & 0 deletions motoko/composite_query/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.PHONY: all
all: build

.PHONY: build
.SILENT: build
build:
dfx canister create --all
dfx build

.PHONY: install
.SILENT: install
install: build
dfx canister install --all

.PHONY: upgrade
.SILENT: upgrade
upgrade: build
dfx canister install --all --mode=upgrade

.PHONY: test
.SILENT: test
test: install
dfx canister call Map test '()'
dfx canister call --query Map get '(15)' \
| grep '(opt "15")' && echo 'PASS'

.PHONY: clean
.SILENT: clean
clean:
rm -fr .dfx
119 changes: 119 additions & 0 deletions motoko/composite_query/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Composite Queries

![Compatibility](https://img.shields.io/badge/compatibility-0.7.0-blue)
[![Build Status](https://github.com/dfinity/examples/workflows/motoko-classes-example/badge.svg)](https://github.com/dfinity/examples/actions?query=workflow%3Amotoko-classes-example)

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, which allow 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](./src/map/Map.mo) imports a Motoko _actor class_ `Bucket(i, n)`
from library [Buckets.mo](./src/map/Buckets.mo).
It also imports the `ExperimentalCycles` base library, naming it `Cycles` for short, in order to share its cycles amongst the bucket 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 `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.


## Security Considerations and Security Best Practices

If you base your application on this example, we recommend you familiarize yourself with and adhere to the [Security Best Practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices.

## Prerequisites

Verify the following before running this demo:

* You have downloaded and installed the
[DFINITY Canister SDK](https://sdk.dfinity.org).

* You have stopped any Internet Computer or other network process that would
create a port conflict on 8000.

## Demo

1. Start a local internet computer.

```text
dfx start
```

(Alternatively, the example will run faster if you use the emulator, not a full replica:
```
dfx start --emulator
```
)

2. Open a new terminal window.

3. Deploy the `Map` canister:

```text
dfx deploy
```

4. Invoke the `test` method of canister `Map` to add some entries

```text
dfx canister call Map test '()'
```

5. Observe the following result.

```text
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")
()
```

6. Invoke the `get` composite query method of canister `Main`

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

7. Observe the following result.

```
(opt "15")
```



# Links

Specific links:

- [Actor classes](https://internetcomputer.org/docs/current/motoko/main/actor-classes.html)
- [Managing Cycles](https://internetcomputer.org/docs/current/motoko/main/cycles.html)
- [Composite Queries](https://internetcomputer.org/docs/current/motoko/main/actors-async#composite-query-functions.html)

14 changes: 14 additions & 0 deletions motoko/composite_query/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 1,
"canisters": {
"Map": {
"type": "motoko",
"main": "src/map/Map.mo"
}
},
"defaults": {
"build": {
"packtool": ""
}
}
}
21 changes: 21 additions & 0 deletions motoko/composite_query/src/map/Buckets.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Nat "mo:base/Nat";
import Map "mo:base/RBTree";

actor class Bucket(n : Nat, i : Nat) {

type Key = Nat;
type Value = Text;

let map = Map.RBTree<Key, Value>(Nat.compare);

public query func get(k : Key) : async ?Value {
assert ((k % n) == i);
map.get(k);
};

public func put(k : Key, v : Value) : async () {
assert ((k % n) == i);
map.put(k, v);
};

};
Loading

0 comments on commit 92eac86

Please sign in to comment.