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 跳脫。
配方
- 取得 schema 一次。
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)。 - 遇到陷阱時重試。 沒有錯誤封裝 — 重新讀取 schema、修正查詢、重新執行(§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的列。Float 相等是位元層級的 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_ |
任何可為 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": ... }}的欄位 — 進入非邊緣欄位的點路徑會觸發陷阱,即使其值看起來像外來鍵(遍歷是 schema 驅動的,而不是名稱猜測)。如果作者沒有宣告邊緣,請回退到下面的兩個查詢模式。 - null 或懸空的外來鍵會將整個點路徑解析為
null(左聯結):該列會失敗所有關係(除了ne),並將儲存格投影為null。 - 從多的一方進行聚合。 跨實體聚合在起始實體的列上執行:從
employee對"department.budget"進行avg是以員工加權的。對於每個部門的數字,從department開始 — 或按點路徑分組並聚合起始實體欄位。 - 選取裸邊緣欄位(
"accountManager")仍然回傳 FK 純量;沒有.*— 為你想要的每個目標欄位命名。
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 — 實體名稱區分大小寫。重新讀取 schema。 |
execute 觸發解析錯誤 |
JSON 格式錯誤(尾隨逗號、單引號),或未作為 Candid 文字字面量跳脫 — 包裹為 ("...") 並將每個內部 " 跳脫為 \"。先用 python3 -m json.tool 驗證 JSON。 |
| 對於預期符合的篩選沒有回傳列 | (1) value 字面量型別不符合欄位的 typeName("5" 用於 Nat);(2) field 拼寫錯誤 — 未知欄位會靜默成為 null_,因此大多數謂詞會失敗;(3) 該欄位在儲存中確實是 null_。 |
gt / lt 跨型別回傳奇怪結果 |
混合型別比較未定義。確保兩個運算元具有相同的 typeName。 |
contains 遺漏你可以看到的列 |
contains / startsWith / endsWith 區分大小寫。對於使用者輸入的搜尋詞,使用 icontains。 |
點路徑觸發 'x' is not an edge of 'y' |
頭段不是宣告的邊緣 — 即使值看起來像 FK,遍歷也是 schema 驅動的。改用兩個查詢的 in 模式。 |
| 跨實體平均值看起來不對 | 聚合在起始實體的列上執行。從你想要平均的實體開始,或按點路徑分組並聚合起始實體欄位。 |
execute 回傳列但缺少欄位 |
你 select 的欄位不在實體中(拼寫錯誤,或被作者隱藏)。從 select 中移除它,或移除 select 以使用預設投影。 |
沒有結構化的錯誤封裝。任何失敗都是一個陷阱 — 修正查詢並重試。






