Query 模式
宣告 mode: "query" 的 command 是一支唯讀程式,並擁有自己的分頁通道。它是模組裡唯一的伺服器端 join 與彙總讀取路徑——紀錄查詢 API 沒有 join——而且它刻意做得很便宜:不加鎖、不寫稽核紀錄、不保留 idempotency key,session 最後直接 rollback。
Authoring 要求
mode: "query" 在存檔時就會驗證,每一項失敗都會指名問題所在。
| 要求 | 原因 |
|---|---|
version: 2 | 扁平的版本 1 DSL 沒有讀取也沒有 outputs。 |
零個 insert / update / delete step | Query command 不能有寫入能力。 |
沒有 callbacks | 沒有東西要通知——根本沒有執行紀錄。 |
沒有生命週期 triggers | 同上。 |
Rules 只能是 before_execute phase | before_commit 沒有 commit 可言。 |
| 至少一個 relation output | 這條通道分頁的是 relation;只有純量 output 的 command 沒東西可分頁。 |
每個 relation output 的產出 select step 都要有非空的 order_by | …requires a deterministic order_by on its producing select step |
因此分頁穩定性是在 authoring 時就被強制的,而不是翻到第 2 頁才發現。
Filter 語意與其他每一條通道相同:空 cell——key 不存在、明確的 JSON null、或空字串 ""——不會被任何值運算子($eq、$neq、$in……)選中,只有 $is_null 會選到它,而且在分頁列裡會投影成 null。因此同一條 predicate 在同一筆 grant 下,query command 回傳的列集合與 REST stored_filters search 相同。
路由
POST /private/module/custom_tables/{scope}/{scope_id}/commands/{command_id}/query{
"inputs": { "status": "open" },
"page": { "limit": 2 }
}page 選填。limit 為 1–100,預設 100。cursor 是 1–512 字元的不透明字串。output 指定要分頁哪一個 relation output,必須符合 ^[a-z0-9_]{1,32}$;當 command 宣告了超過一個 relation output 時它是必填的(400 page.output is required for a command with multiple relation outputs;名稱錯誤是 400 page.output does not name a relation output)。
/query 與 /execute 一樣不接受 acting_chatroom_id。該參數已於 2026-07-29 從三個掛載點的這兩個操作上移除,company 掛載點的 422 也一併消失;現在送出它會拿到 200 並被忽略。Query 讀到的每一張受管制資料表,都各自從呼叫者的房間解析自己的 channel floor——見手動執行時的 channel scope。
路由與模式必須一致,而且這個檢查在任何 idempotency 探測或稽核紀錄寫入之前就執行。對 query command 呼叫 POST /execute 是 409 {"error":"query_command_requires_query_route"};對寫入型 command 呼叫 POST /query 是 409 {"error":"not_a_query_command"}。因此走錯路由的讀取永遠不會保留 replay key,也不會留下失敗的稽核紀錄。
回應
{
"rows": [
{ "order_no": "SO-2026-0002", "customer": "Northwind", "amount": 4200, "created": "2026-07-24T09:11:02" },
{ "order_no": "SO-2026-0001", "customer": "Contoso", "amount": 1200, "created": "2026-07-23T14:02:44" }
],
"outputs": {},
"next_cursor": "eyJkIjoiOWYyYzFiM2E0ZDVlNmY3MCIsIm8iOjJ9",
"total_available": 7
}rows是被分頁的那個 relation output 的一頁,最多 100 筆。outputs裝的是其餘所有宣告的 output——純量以及其他 relation output,各自整形到min(100, max_rows)列。只有正在分頁的那一個會被跳過。next_cursor只在還有後續列時出現;最後一頁時為 null,因此不會出現在 body 裡。total_available是該 relation 的總列數,永遠不會超過 1000。
擁有兩個 relation output 的 query command 會分頁其中一個,並在每一頁把另一個整份回傳。請只宣告某一頁真正需要的東西,否則同一份載荷會跟著每一次請求一起送。
Cursor 是綁定,不是書籤
Cursor 是對 {"o": <offset>, "d": <digest>} 做 url-safe base64,其中 digest 是 sha256(dependency contract 摘要 ‖ 正規化後的已驗證 inputs ‖ output 名稱) 的前 16 個十六進位字元。把 next_cursor 當成 page.cursor 傳回來就能取得下一頁。
Command 的相依、inputs 或所請求的 output 只要有任何改變,它就失效:400 {"error":"cursor_binding_mismatch"}。格式錯誤、超過 512 字元、offset 為負數或非整數,也都回同一個 400。
伺服器端沒有任何 cursor 狀態。每一頁都會從頭重跑整支程式,並在記憶體中對完整實體化的 relation 開窗(rows[offset : offset + limit]),所以兩頁之間插入的一列會讓視窗位移。摘要綁定的是契約、inputs 與 output 名稱——不是資料。Cursor 不是持久書籤;發生任何漂移後請重新從第一頁開始。
由於每一頁都會完整實體化整個 relation,所有頁數加總永遠不會超過 1000 列的 relation 上限。Query 模式是讀取畫面用的,不是批次匯出或報表 API。
不加鎖,不留痕跡
Query 通道從不進入 authority lock 情境、跳過被消耗紀錄的 FOR UPDATE 圍籬、不寫執行稽核紀錄、不保留 idempotency key,最後一律 rollback。它可以安全重試,也可以放在熱路徑上呼叫。
不要叫使用者去執行清單裡找某次 query。那裡什麼都沒有——query 完全不留稽核紀錄。
其餘規則仍然適用。Query command 就是版本 2 command,所以每次呼叫都會重新驗證完整的 dependency contract,被引用資料表漂移時回 409 schema_dependency_changed。可見性一樣要求對每一張被引用資料表有讀取權、且 tag 仍然有效;呼叫者讀不到的 SELECT 來源或 JOIN 資料表,現在是 403 "Read access not granted for this table.",不再是空的 relation。
兩條通道之間有一個狀態碼不同:query 通道的逐頁位元組檢查拋出 422 output_limit_exceeded,而 /execute 上同一個錯誤碼是 400。請對 error 值分支,不要只看狀態碼。
完整範例:待處理訂單畫面
{
"name": "orders_by_status",
"description": "列出某個狀態的訂單,由新到舊,並帶出客戶名稱。",
"tag_id": "55555555-5555-4555-8555-555555555555",
"definition": {
"version": 2,
"mode": "query",
"inputs": [
{ "name": "status", "type": "string", "required": true, "nullable": false, "max_length": 32 }
],
"steps": [
{
"kind": "select",
"name": "open_orders",
"from": { "table": "Orders", "as": "o" },
"joins": [
{
"type": "left",
"table": "Customers",
"as": "c",
"on": { "and": [{ "left": "$row.o.Customer", "op": "eq", "right": "$row.c.id" }] }
}
],
"select": {
"order_no": "$row.o.Order no",
"customer": "$row.c.Name",
"amount": "$row.o.Amount",
"created": { "$metadata": { "source": "o", "name": "created_at" } }
},
"where": { "$eq": ["$row.o.Status", "$input.status"] },
"order_by": [
{ "expr": { "$metadata": { "source": "o", "name": "created_at" } }, "direction": "desc" }
],
"max_rows": 1000
}
],
"outputs": {
"orders": {
"relation": "$rel.open_orders",
"select": ["order_no", "customer", "amount", "created"],
"max_rows": 100
}
}
}
}只有一個 relation output,所以 page.output 可以省略。order_by 宣告在產出它的 select 上——這正是這個 command 能被存檔的前提。
Query 模式與 AI agent
Query 模式的 command 會產生一個多帶一個選填 cursor 參數的 agent 工具。該工具只送 page: {cursor}、從不送 limit,所以每一頁 agent 驅動的查詢都是預設的 100 列;它同時會在轉送前把 cursor 從 inputs 中移除。若要把 query command 開放給 agent,請以此為前提設定 relation 大小。
延伸閱讀
- 定義版本 2——query command 使用的程式語言。
- 執行語意——寫入通道,以及 query 模式刻意略過的每一件事。
- Commands 參考——路由、參數與完整錯誤表。