Quick reference for the Caffeine Data Intelligence agent to query an OQL-exposing canister (schema() + execute()) through the `icp` CLI against the project's `backend` canister: read the schema, form JSON queries (filter / order / paginate / aggregate / dotted-path edges), and parse the Candid result rows.
查询 OQL — 快速参考
OQL 容器暴露两个只读方法:
| 方法 | 返回类型 | 用途 |
|---|---|---|
schema() |
一个 JSON Text |
容器实体的目录:每个实体的主键、字段和边。 |
execute(qJson : text) |
类型化的 Candid Result |
运行 JSON 编码的查询并返回匹配的行。 |
调用容器
icp CLI 已在沙箱中安装并配置;容器名称 backend 解析为项目的容器(无需身份,无需容器 ID)。两个方法都是 query 调用,因此每次调用都使用 --query:
icp canister call backend schema '()' --query
icp canister call backend execute '("<json-query>")' --query
execute 接受一个 text 参数 — 嵌入为 Candid 文本字面量的 JSON 查询。将 JSON 包裹在 ("...") 中,并将每个 " 转义为 \"。查询 {"start":"customer","limit":3} 变为:
icp canister call backend execute '("{\"start\":\"customer\",\"limit\":3}")' --query
schema() 以相同方式返回其 JSON — 一个 Candid text 字面量 ("...escaped json...");取消转义 \" → "(以及 \\ → \)以读取。添加 --branch live 以读取已部署的容器而不是草稿(live 仅查询)。如果字符串值包含单引号,请使用 '\'' 为 shell 转义。
配方
- 获取模式一次。
icp canister call backend schema '()' --query— 在会话中缓存;它仅在部署之间更改(§1)。 - 将请求映射到实体。 选择包含答案的实体。使用每个字段的
typeName和values选择字面量类型,并使用role: {"edge": ...}查看实体如何连接。 - 转换为一个或多个查询。 从您想要行的实体开始(§2)。添加
where(§2.1)、orderBy/limit/offset、select和aggregate/groupBy(§2.2)。在单个查询中使用点路径跨越前向边(§4.1);反向一对多需要先获取父键,然后使用in(§4.2)。 - 运行并读取。
icp canister call backend execute '("<json>")' --query— 按单元格name解析 Candid 行(§3);如果hasMore,使用offset分页(§5)。 - 出错重试。 没有错误信封 — 重新读取模式,修复查询,重新运行(§6)。
1. 发现 — schema
获取一次并在会话中缓存 — 它仅在容器部署之间更改。
icp canister call backend schema '()' --query
如下读取:
name→ 实体名称;在查询中用作start。primaryKey→ 其值标识一行的字段。边{"to": "<entity>"}的值是该目标中的主键值。fields→ 每个字段的name、标量typeName和role:"payload"(普通字段)或{"edge": {"to": "<entity>"}}(外键 — 如何遍历图)。当两列共享名称时,名称可能带有__1、__2、… 后缀 — 使用schema()报告的确切名称。values(可选)→ 字段可以持有的确切字面量(通常是变体的分支)。使用这些字面量过滤,而不是猜测:["free","pro","enterprise"]表示查询"enterprise",而不是"Enterprise"。缺失 ⇒ 无界 — 如果需要候选值,使用查询采样。typeName→value的 JSON 字面量类型:"Nat"→ 无符号整数(0、1、…)"Int"→ 有符号整数(-1、0、1、…)"Float"→ 带小数点的 JSON 数字(0.5、-3.14、1.0e2)。裸整数(10)也被接受 — 数字变体桥接,因此gt(price, 10)匹配price : Float = 12.5的行。浮点相等是按位 IEEE-754;对于没有精确二进制形式的十进制数(如0.42),使用范围(ge+le)。"Bool"→true/false"Text"→ JSON 字符串。Principal字段报告为"Text"(规范文本形式)— 使用字符串值过滤。
2. 构造查询 — execute
查询是一个 JSON 对象。只有 start 是必需的。
{
"start": "<entityName>",
"where": <Predicate>,
"groupBy": ["<fieldName>", ...],
"aggregate": [{ "fn": "count|sum|avg|min|max", "field": "<fieldName>", "as": "<outName>" }, ...],
"orderBy": [{ "field": "<fieldName>", "dir": "asc|desc" }, ...],
"offset": <Nat>,
"limit": <Nat>,
"select": ["<fieldName>", ...]
}
| 字段 | 默认值 | 说明 |
|---|---|---|
start |
(必需) | 来自 schema() 的实体 name。 |
where |
省略 ⇒ 无过滤 | 单个谓词(§2.1)— 不包裹在 {"filter": ...} 中。 |
groupBy |
[] |
按这些字段分桶行;每个不同组合一行输出(§2.2)。 |
aggregate |
[] |
每个桶的聚合,或当 groupBy 为空时对所有行聚合(§2.2)。 |
orderBy |
[](容器定义的顺序,通常是插入顺序) |
多键排序,第一个子句为主。dir 默认为 "asc"。 |
offset |
0 |
丢弃前 N 个匹配。 |
limit |
所有匹配 | 最多保留 N 个。结果中的 hasMore 告诉您是否还有更多。 |
select |
每个非隐藏字段(或聚合时,分组键 + 聚合列) | 子集投影。 |
icp canister call backend execute '("{\"start\":\"customer\",\"limit\":3}")' --query
过滤 + 排序 + 投影 — 核心形状(where + orderBy + limit + select):
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"eq\":{\"field\":\"plan\",\"value\":\"enterprise\"}},\"orderBy\":[{\"field\":\"monthlyRevenueUsd\",\"dir\":\"desc\"}],\"limit\":5,\"select\":[\"companyName\",\"monthlyRevenueUsd\",\"accountManagerName\"]}")' --query
2.1 谓词操作符
Predicate 是一个 JSON 对象,恰好有一个键命名操作符。
| 操作符 | 形状 | 含义 |
|---|---|---|
eq / ne / lt / le / gt / ge |
{"<op>": { "field": "<name>", "value": <scalar> } } |
标量关系。 |
in |
{"in": { "field": "<name>", "value": [<scalar>, ...] } } |
成员资格;空数组不匹配任何内容。 |
contains / startsWith / endsWith |
{"<op>": { "field": "<name>", "value": "<text>" } } |
区分大小写的子字符串/前缀/后缀,作用于 Text — 服务器端扫描,无需将行分页到上下文中。 |
icontains |
{"icontains": { "field": "<name>", "value": "<text>" } } |
不区分大小写的 contains。对于用户输入的搜索词,优先使用此操作符。 |
and / or / not |
{"and": [<P>, ...]} / {"or": [<P>, ...]} / {"not": <P>} |
布尔组合。 |
文本搜索在服务器端运行 — "名称包含 north 的客户" 是一个查询,而不是行扫描到上下文中:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"icontains\":{\"field\":\"companyName\",\"value\":\"north\"}},\"select\":[\"companyName\",\"accountManagerName\"]}")' --query
<scalar> 必须匹配字段的 typeName:
| JSON | 映射到 | 用于 typeName 为以下值的字段 |
|---|---|---|
null |
null_ |
任何可空字段(在 where 中很少见) |
true / false |
bool |
"Bool" |
0、1、42 |
nat |
"Nat"(也通过数字桥接匹配 "Float") |
-1、-42 |
int |
"Int"(也通过数字桥接匹配 "Float") |
0.5、-3.14、1.0e2 |
float |
"Float" |
"foo" |
text |
"Text" |
字段值为 null_ 的行不满足任何关系,除了 ne。通过 field = "<edge>" 和 value = 目标实体的主键值按关系过滤;或通过 "<edge>.<targetField>" 读取通过边(§4.1)。
2.2 聚合 — count、groupBy、sum/avg/min/max
在容器上计算,而不是获取每一行并在客户端统计。fn 是 count/sum/avg/min/max;除 count 外,每个 fn 都需要 field;min/max 也适用于文本。as 重命名输出列(默认为 count、sum_<field>、…),并且不能包含 .(点是边遍历分隔符 — 解析错误)。对于带点的 field,默认使用 _ 连接段(dept.budget 的 sum → sum_dept_budget)。没有 groupBy 的 aggregate → 对整个过滤集的一行(空匹配的 count 为 0)。没有 aggregate 的 groupBy → 服务器端 DISTINCT。输出行仅包含分组键 + 聚合列。
"有多少企业客户?" — 对过滤集进行 count,输出一行:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"eq\":{\"field\":\"plan\",\"value\":\"enterprise\"}},\"aggregate\":[{\"fn\":\"count\"}]}")' --query
"哪个客户经理拥有最多客户,以及总 MRR?" — groupBy + count + sum:
icp canister call backend execute '("{\"start\":\"customer\",\"groupBy\":[\"accountManager\"],\"aggregate\":[{\"fn\":\"count\"},{\"fn\":\"sum\",\"field\":\"monthlyRevenueUsd\",\"as\":\"mrr\"}],\"orderBy\":[{\"field\":\"count\",\"dir\":\"desc\"}],\"limit\":1}")' --query
3. 读取结果
type Value = variant { null_; bool : bool; nat : nat; int : int; float : float; text : text };
type Cell = record { name : text; value : Value };
type Result = record { rows : vec vec Cell; hasMore : bool };
外层的 rows = vec { ... } 是行列表;每个内层的 vec { ... } 是一行。每个 record { value = variant { "<tag>" = <payload> }; name = "<field>" } 是一个单元格 — name 告诉您哪个字段,<tag> 告诉您标量类型,payload 是值。35_000 : nat 中的下划线是数字分隔符 — 解析时去掉。hasMore = false ⇒ 您得到了所有匹配;hasMore = true ⇒ 被截断,获取下一页。按 name 查找单元格,而不是位置 — 如果 select 更改,顺序会改变。
4. 遍历边(连接)
前向(单值)关系是一个查询:点路径在任何字段位置跨越声明的边。反向(一对多)关系保持两个查询,使用 in 模式(§4.2)。
4.1 前向(子 → 父):点路径
"<edgeField>.<targetField>" 在服务器端通过边读取 — 在 where、groupBy、orderBy、aggregate.field 和 select 中。在一个查询中通过边投影:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"eq\":{\"field\":\"companyName\",\"value\":\"Northstar Public\"}},\"select\":[\"companyName\",\"accountManager.name\",\"accountManager.office\"]}")' --query
多跳链有效("manager.department.name",最多 4 跳),并且可以与聚合组合 — "按客户经理办公室的平均收入" 是一个调用:
icp canister call backend execute '("{\"start\":\"customer\",\"groupBy\":[\"accountManager.office\"],\"aggregate\":[{\"fn\":\"avg\",\"field\":\"monthlyRevenueUsd\",\"as\":\"avg_mrr\"}],\"orderBy\":[{\"field\":\"avg_mrr\",\"dir\":\"desc\"}]}")' --query
规则:
- 头段必须是
schema()中role为{"edge": {"to": ... }}的字段 — 进入非边字段的点路径会出错,即使其值看起来像外键(遍历是模式驱动的,而不是名称猜测的)。如果作者没有声明边,则回退到下面的双查询模式。 - 空或悬空的外键将整个点路径解析为
null(左连接):该行不满足任何关系,除了ne,并将单元格投影为null。 - 从多侧聚合。 跨实体聚合在起始实体的行上运行:从
employee对"department.budget"的avg是员工加权的。对于每个部门的数字,从department开始 — 或按点路径分组并聚合起始实体字段。 - 选择裸边字段(
"accountManager")仍然返回外键标量;没有.*— 命名您想要的每个目标字段。
4.2 反向(一个父 → 多个子)
eq 用于一个父主键,in 用于一批 — 在边字段上,使用目标实体的主键值。
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"eq\":{\"field\":\"accountManager\",\"value\":\"daniel@helix.systems\"}},\"select\":[\"companyName\",\"monthlyRevenueUsd\"]}")' --query
当父条件是一个普通谓词时,您不需要批处理 — 它是通过边的前向过滤(§4.1)。"由柏林办公室任何人管理的所有客户" 是一个查询:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"eq\":{\"field\":\"accountManager.office\",\"value\":\"Berlin\"}},\"select\":[\"companyName\",\"monthlyRevenueUsd\"]}")' --query
当父集需要其自己的查询形状(Top-N、排序、分页)时,需要批处理 in 模式:首先收集键,然后在边字段上使用 in。"由三位最资深员工管理的客户" 是两个查询:
icp canister call backend execute '("{\"start\":\"employee\",\"orderBy\":[{\"field\":\"level\",\"dir\":\"desc\"}],\"limit\":3,\"select\":[\"email\"]}")' --query
# 从行中收集三个电子邮件,然后:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"in\":{\"field\":\"accountManager\",\"value\":[\"alex@helix.systems\",\"james@helix.systems\",\"sarah@helix.systems\"]}},\"select\":[\"companyName\",\"monthlyRevenueUsd\"]}")' --query
始终使用 in 批处理,而不是运行 N 个单独的 eq 查询。
4.3 复合条件
使用 and / or 堆叠:
icp canister call backend execute '("{\"start\":\"customer\",\"where\":{\"and\":[{\"eq\":{\"field\":\"plan\",\"value\":\"enterprise\"}},{\"in\":{\"field\":\"country\",\"value\":[\"US\",\"CA\",\"DE\"]}},{\"ge\":{\"field\":\"monthlyRevenueUsd\",\"value\":20000}}]},\"orderBy\":[{\"field\":\"monthlyRevenueUsd\",\"dir\":\"desc\"}]}")' --query
4.4 两跳/自边连接
当父键未给出但必须首先查找时 — 例如 "谁向项目 forge20 的负责人报告?" — 运行两个查询。第二个查询通过第一个查询返回的键在自边(employee.manager → employee)上过滤:
icp canister call backend execute '("{\"start\":\"project\",\"where\":{\"eq\":{\"field\":\"codename\",\"value\":\"forge20\"}},\"select\":[\"lead\"]}")' --query
# 行的 `lead` 单元格是负责人的电子邮件,例如 priya@helix.systems — 将其用作父键:
icp canister call backend execute '("{\"start\":\"employee\",\"where\":{\"eq\":{\"field\":\"manager\",\"value\":\"priya@helix.systems\"}},\"select\":[\"name\",\"jobTitle\",\"level\"]}")' --query
5. 分页
limit 限制结果。hasMore 报告截断。使用 offset 遍历页面:
offset = 0
limit = 25
loop:
result = icp canister call backend execute '("{\"start\":\"...\",\"limit\":25,\"offset\":<offset>,...}")' --query
consume result.rows
if not result.hasMore: break
offset += limit
始终显式设置 limit。OQL 本身没有上限(省略 limit 返回所有匹配),容器作者可能添加一个 — 在这种情况下,过度请求会被静默截断。
6. 陷阱
| 症状 | 原因/修复 |
|---|---|
execute 出错 OQL: unknown entity '...' |
start 不匹配 schema() 中的任何 name — 实体名称区分大小写。重新读取模式。 |
execute 因解析错误而出错 |
JSON 格式错误(尾随逗号、单引号),或未作为 Candid 文本字面量转义 — 包裹为 ("..."),每个内部 " 转义为 \"。首先使用 python3 -m json.tool 验证 JSON。 |
| 对于您期望匹配的过滤器,没有返回行 | (1) value 字面量类型不匹配字段的 typeName(Nat 字段使用 "5");(2) field 拼写错误 — 未知字段静默为 null_,因此大多数谓词失败;(3) 字段在存储中确实为 null_。 |
gt / lt 在类型间返回奇怪的结果 |
混合类型比较未定义。确保两个操作数具有相同的 typeName。 |
contains 遗漏了您可以看到的行 |
contains / startsWith / endsWith 区分大小写。对于用户输入的搜索词,使用 icontains。 |
点路径出错 'x' is not an edge of 'y' |
头段不是声明的边 — 即使值看起来像外键,遍历也是模式驱动的。改用双查询 in 模式。 |
| 跨实体平均值看起来不对 | 聚合在起始实体的行上运行。从您想要平均的实体开始,或按点路径分组并聚合起始实体字段。 |
execute 返回行但缺少字段 |
您 select 的字段不在实体中(拼写错误,或被作者隐藏)。从 select 中删除它,或删除 select 以使用默认投影。 |
没有结构化的错误信封。任何失败都是陷阱 — 修复查询并重试。






