Appearance
Expressions
Everything you can write inside an operator: arithmetic, comparison, conditions, member access, calls on contracts, raw calls, storage reads and casts.
Arithmetic
+, -, *, / and % on numbers, with the usual precedence; -x negates. Whole-number division truncates; a division involving a decimal is computed to 38 digits. Numbers has the detail. + is not concatenation — concat(a, b) joins strings and bytes.
Comparison
== and != between any two values of one type; <, <=, >, >= between numbers, times, durations and same-width bytes. A chain of comparisons, a < b < c, is refused: write a < b and b < c.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
from stETH at latest-21600..latest every 7200 as s
| extend pooled = s.getTotalPooledEther(), shares = s.getTotalShares()
| where pooled >= shares and shares != 0
| project { block: $block, pooled, shares }and, or, not
Three-valued: null and false is false, null or true is true, and any other mix with null is null. not binds more loosely than a comparison, so not a == b means not (a == b).
The conditional
iff(c, a, b) gives a when c is true and b otherwise, null included. c ? a : b is the same thing in another spelling. Only the chosen branch is evaluated, so a call in the other branch costs nothing.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
from stETH at latest-21600..latest every 7200 as s
| project { block: $block, staking: s.isStakingPaused() ? 'paused' : 'open' }Put a space after the : when the branch after it starts with 0x; without one, b:0x… reads as an address.
in and between
x in (a, b, c) asks whether x equals one of the listed values. x in xs — no parentheses — asks whether x is an element of the array xs. x between (lo .. hi) includes both bounds.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
let treasury = ethereum:0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c;
from stETH at latest-21600..latest every 7200 as s
| extend paidTo = s.getTreasury(), ethPerShare = format(s.getPooledEthByShares(1e18), 18)
| where paidTo in (treasury, wstETH) and ethPerShare between (1 .. 2)
| project { block: $block, ethPerShare }Members and calls
. reads a field or calls a function. e.reportTimestamp is an event argument; s.getTotalShares() calls the contract at the row's block; s.$address is a system column of that source; r.field reads a field of a record; xs[0] indexes an array, counted from zero.
A call's result is typed from the ABI: a single output is the value, several named outputs are a record, several unnamed are _0, _1, …. A call the contract reverts gives null, and revert_reason(s.getTotalShares()) says why.
.call() — a call without an ABI
Not available in the workbench yet. x.call(sig, args…) calls any function by its signature and needs no ABI at all:
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
from stETH with { abi: none } as s
| project {
totalShares: s.call('getTotalShares()(uint256)'),
shares: s.call('sharesOf(address)(uint256)', wstETH),
raw: s.call('getTotalShares()'),
bySel: s.call('0xd5002f2e', '(uint256)')
}The signature is name(inputs)(outputs). Leave the outputs off and you get raw bytes; give a 4-byte selector instead of a name and the outputs become a second argument. Named outputs make a record. .call is also the way past an ambiguous overload (CQL2011).
$storage — raw storage
x.$storage[slot] reads one 32-byte word of a contract's storage at the row's block. The slot is a number or a bytes32; the result is bytes32, which you usually cast:
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
from stETH as s
| project {
kernel: s.$storage[keccak256('aragonOS.appStorage.kernel')] as address,
elRewards: s.$storage[keccak256('lido.Lido.totalELRewardsCollected')] as uint256
}stETH keeps its state in named slots, so keccak256 of the name is the slot. For a mapping entry, an array element or a struct member, slot() computes the slot the way Solidity lays them out — Functions.
Casts
x as T reinterprets a 32-byte word: as address, as uint256 (or as bigint), as int128, as bytes4, as bool, as string. It is a reinterpretation, not a conversion; to turn a decimal into a bigint use tobigint. This as is not the one that names a source.
Options on one call
s.transfer(nobody, 1e18) with { caller: wstETH } sets the options of that call alone — who is asking, at which block, with what value, which ABI. with options.
Backticks
A column whose name is a keyword — to is fine, from is not — is written in backticks: `from`. The backticks change nothing else; `value` and value are the same column.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
from stETH at latest-21600..latest every 7200 as s
| project { `from`: $block, ethPerShare: format(s.getPooledEthByShares(1e18), 18) }
| where `from` > 0If you know CEL
CQL's expressions look like Google's CEL, and three things read differently:
| Text | In CEL | In CQL |
|---|---|---|
0xFF | an integer | bytes — tobigint(0xFF) for the number |
not a == b | (not a) == b | not (a == b) |
1.5 | a double | an exact decimal |
a || b and a && b are refused; write or and and.