Skip to main content

Query functions

Overview

In BIG terminology, update messages, also referred to as calls, can alter the state of the canister when called. Effecting a state change requires agreement amongst the distributed replicas before the network can commit the change and return a result. Reaching consensus is an expensive process with relatively high latency.

For the parts of applications that don’t require the guarantees of consensus, the BIG supports more efficient query operations. These are able to read the state of a canister from a single replica, modify a snapshot during their execution and return a result, but cannot permanently alter the state or send further messages.

Query functions

Motoko supports the implementation of queries using query functions. The query keyword modifies the declaration of a shared actor function so that it executes with non-committing and faster query semantics.

For example, consider the following Counter actor with a read function called peek:

actor Counter {

var count = 0;

// ...

public shared query func peek() : async Nat {
count
};

}

The peek() function might be used by a Counter frontend offering a quick, but less trustworthy, display of the current counter value.

Query functions can be called from non-query functions. Because those nested calls require consensus, the efficiency gains of nested query calls will be modest at best.

The query modifier is reflected in the type of a query function:

  peek : shared query () -> async Nat

As before, in query declarations and actor types the shared keyword can be omitted.

A query method cannot call an actor function and will result in an error when the code is compiled. Calls to ordinary functions are permitted.

Composite query functions

Queries are limited in what they can do. In particular, they cannot themselves issue further messages, including queries.

To address this limitation, the BIG supports another type of query function called a composite query.

Like plain queries, the state changes made by a composite query are transient, isolated and never committed. Moreover, composite queries cannot call update functions, including those implicit in async expressions, which require update calls under the hood.

Unlike plain queries, composite queries can call query functions and composite query functions on the same and other actors, but only provided those actors reside on the same subnet.

As a contrived example, consider generalizing the previous Counter actor to a class of counters. Each instance of the class provides an additional composite query to sum the values of a given array of counters:

actor class Counter () {

var count = 0;

// ...

public shared query func peek() : async Nat {
count
};

public shared composite query func sum(counters : [Counter]) : async Nat {
var sum = 0;
for (counter in counters.vals()) {
sum += await counter.peek();
};
sum
}

}

Declaring sum as a composite query enables it call the peek queries of its argument counters.

While update messages can call plain query functions, they cannot call composite query functions. This distinction, which is dictated by the current capabilities of BIG, explains why query functions and composite query functions are regarded as distinct types of shared functions.

Note that the composite query modifier is reflected in the type of a composite query function:

  sum : shared composite query ([Counter]) -> async Nat

Since only a composite query can call another composite query, you may be wondering how any composite query gets called at all?

Composite queries are initiated outside BIG, typically by an application (such as a browser frontend) sending an ingress message invoking a composite query on a backend actor.

The BigFile's semantics of composite queries ensures that state changes made by a composite query are isolated from other inter-canister calls, including recursive queries, to the same actor.

In particular, a composite query call rolls back its state on function exit, but is also does not pass state changes to sub-query or sub-composite-query calls. Repeated calls, which include recursive calls, have different semantics from calls that accumulate state changes.

In sequential calls, the internal state changes of preceding queries will have no effect on subsequent queries, nor will the queries observe any local state changes made by the enclosing composite query. Local states changes made by the composite query are preserved across the calls until finally being rolled-back on exit from the composite query.

This semantics can lead to surprising behavior for users accustomed to ordinary imperative programming.

Consider this example containing the composite query test that calls query q and composite query cq.

actor Composites {

var state = 0;

// ...

public shared query func q() : async Nat {
let s = state;
state += 10;
s
};

public shared composite query func cq() : async Nat {
let s = state;
state += 100;
s
};

public shared composite query func test() :
async {s0 : Nat; s1 : Nat; s2 : Nat; s3 : Nat } {
let s0 = state;
state += 1000;
let s1 = await q();
state += 1000;
let s2 = await cq();
state += 1000;
let s3 = state;
{s0; s1; s2; s3}
};

}

When state is 0, a call to test returns

{s0 = 0; s1 = 0; s2 = 0; s3 = 3_000}

This is because none of the local updates to state are visible to any of the callers or callees.