extension-querying-oql

extension-querying-oql

Caffeine Data Intelligence 智能体查询 OQL 暴露的容器(schema() + execute())的快速参考:通过 `icp` CLI 针对项目的 `backend` 容器读取模式、构造 JSON 查询(过滤/排序/分页/聚合/点路径边),并解析 Candid 结果行。

0Star
0Fork
更新于 2026/7/13
SKILL.md
readonly只读
name
extension-querying-oql
description

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.

version
0.4.0

查询 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 转义。


配方

  1. 获取模式一次。 icp canister call backend schema '()' --query — 在会话中缓存;它仅在部署之间更改(§1)。
  2. 将请求映射到实体。 选择包含答案的实体。使用每个字段的 typeNamevalues 选择字面量类型,并使用 role: {"edge": ...} 查看实体如何连接。
  3. 转换为一个或多个查询。 从您想要行的实体开始(§2)。添加 where(§2.1)、orderBy / limit / offsetselectaggregate / groupBy(§2.2)。在单个查询中使用点路径跨越前向边(§4.1);反向一对多需要先获取父键,然后使用 in(§4.2)。
  4. 运行并读取。 icp canister call backend execute '("<json>")' --query — 按单元格 name 解析 Candid 行(§3);如果 hasMore,使用 offset 分页(§5)。
  5. 出错重试。 没有错误信封 — 重新读取模式,修复查询,重新运行(§6)。

1. 发现 — schema

获取一次并在会话中缓存 — 它仅在容器部署之间更改。

icp canister call backend schema '()' --query

如下读取:

  • name → 实体名称;在查询中用作 start
  • primaryKey → 其值标识一行的字段。边 {"to": "<entity>"} 的值是该目标中的主键值。
  • fields → 每个字段的 name、标量 typeNamerole"payload"(普通字段)或 {"edge": {"to": "<entity>"}}(外键 — 如何遍历图)。当两列共享名称时,名称可能带有 __1__2、… 后缀 — 使用 schema() 报告的确切名称。
  • values(可选)→ 字段可以持有的确切字面量(通常是变体的分支)。使用这些字面量过滤,而不是猜测:["free","pro","enterprise"] 表示查询 "enterprise",而不是 "Enterprise"。缺失 ⇒ 无界 — 如果需要候选值,使用查询采样。
  • typeNamevalue 的 JSON 字面量类型:
    • "Nat" → 无符号整数(01、…)
    • "Int" → 有符号整数(-101、…)
    • "Float" → 带小数点的 JSON 数字(0.5-3.141.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"
0142 nat "Nat"(也通过数字桥接匹配 "Float"
-1-42 int "Int"(也通过数字桥接匹配 "Float"
0.5-3.141.0e2 float "Float"
"foo" text "Text"

字段值为 null_ 的行不满足任何关系,除了 ne。通过 field = "<edge>"value = 目标实体的主键值按关系过滤;或通过 "<edge>.<targetField>" 读取通过边(§4.1)。

2.2 聚合 — count、groupBy、sum/avg/min/max

在容器上计算,而不是获取每一行并在客户端统计。fncount/sum/avg/min/max;除 count 外,每个 fn 都需要 fieldmin/max 也适用于文本。as 重命名输出列(默认为 countsum_<field>、…),并且不能包含 .(点是边遍历分隔符 — 解析错误)。对于带点的 field,默认使用 _ 连接段(dept.budgetsumsum_dept_budget)。没有 groupByaggregate → 对整个过滤集的一行(空匹配的 count0)。没有 aggregategroupBy → 服务器端 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>" 在服务器端通过边读取 — 在 wheregroupByorderByaggregate.fieldselect 中。在一个查询中通过边投影:

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.manageremployee)上过滤:

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 字面量类型不匹配字段的 typeNameNat 字段使用 "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 以使用默认投影。

没有结构化的错误信封。任何失败都是陷阱 — 修复查询并重试。