extension-querying-oql

extension-querying-oql

Caffeine Data Intelligence 代理程式快速參考,用於透過 `icp` CLI 查詢專案 `backend` 容器中暴露 OQL 的容器(schema() + execute()):讀取 schema、建構 JSON 查詢(篩選/排序/分頁/聚合/點路徑邊緣),並解析 Candid 結果列。

0星標
0分支
更新於 2026/7/13
SKILL.md
readonlyread-only
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. 取得 schema 一次。 icp canister call backend schema '()' --query — 在工作階段中快取它;它只會在部署之間變更(§1)。
  2. 將請求對應到實體。 選取包含答案的實體。使用每個欄位的 typeNamevalues 來選擇字面量型別,並使用 role: {"edge": ...} 來查看實體如何連接。
  3. 轉譯為一個或多個查詢。 從你想要列的實體開始(§2)。加入 where(§2.1)、orderBy / limit / offsetselect 以及 aggregate / groupBy(§2.2)。在單一查詢中使用點路徑跨越正向邊緣(§4.1);反向一對多需要先取得父鍵,然後使用 in(§4.2)。
  4. 執行並讀取。 icp canister call backend execute '("<json>")' --query — 透過儲存格 name 解析 Candid 列(§3);如果 hasMore,使用 offset 分頁(§5)。
  5. 遇到陷阱時重試。 沒有錯誤封裝 — 重新讀取 schema、修正查詢、重新執行(§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 的列。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"
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": ... }} 的欄位 — 進入非邊緣欄位的點路徑會觸發陷阱,即使其值看起來像外來鍵(遍歷是 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.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 — 實體名稱區分大小寫。重新讀取 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 以使用預設投影。

沒有結構化的錯誤封裝。任何失敗都是一個陷阱 — 修正查詢並重試。