Skip to Content

Query 模式

宣告 mode: "query" 的 command 是一支唯讀程式,並擁有自己的分頁通道。它是模組裡唯一的伺服器端 join 與彙總讀取路徑——紀錄查詢 API 沒有 join——而且它刻意做得很便宜:不加鎖、不寫稽核紀錄、不保留 idempotency key,session 最後直接 rollback。

Authoring 要求

mode: "query" 在存檔時就會驗證,每一項失敗都會指名問題所在。

要求原因
version: 2扁平的版本 1 DSL 沒有讀取也沒有 outputs。
零個 insert / update / delete stepQuery command 不能有寫入能力。
沒有 callbacks沒有東西要通知——根本沒有執行紀錄。
沒有生命週期 triggers同上。
Rules 只能是 before_execute phasebefore_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,預設 100cursor 是 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 /execute409 {"error":"query_command_requires_query_route"};對寫入型 command 呼叫 POST /query409 {"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 大小。

延伸閱讀

Last updated on