Skip to Content

定義版本 2

版本 2 把 command 變成一支小型的關聯式程式。Steps 以 kind 區分,讀取是第一級公民,而值只能透過宣告的 outputs 對映離開 command。Join、彙總、outputs 與 query 模式都需要它。

{ "version": 2, "mode": "write", "inputs": [], "rules": [], "callbacks": [], "triggers": [], "steps": [], "outputs": {} }

省略 version 的定義中帶 kind 的 step,會被以 kind-discriminated steps require explicit version 2 拒絕;在明確的 version: 1 下則是 steps[i] ('name'): version 1 requires action-based steps — "kind" is version-2 only. Use action + rows, or declare version 2.。在版本 2 下的扁平 action step 會被以 steps[i] ('name'): version 2 requires kind-discriminated steps … 拒絕,未知的 kind 則是 steps[i] ('name'): unknown step kind '<kind>'. version 2 requires kind-discriminated steps — valid kinds are …。這四種都是建立、更新與 validate 時的 request 驗證——loc ["body","definition"] 底下的 FastAPI 422 項目,msg 為 Value error, <message>——而不是程式錯誤信封。明確寫出的 mode: "write" 儲存時會被丟掉——不要期待它被回傳。

v2 step 的 kind 為 list/object 時,是另一種帶型別的 400 issue:command_step_kind_type、loc: ["body","definition"]、msg: "Command version 2 step kind must be a scalar"。issue 不含 input 或 ctx;其他 request 驗證失敗仍保留既有的 422 契約。

外層另外接受選填、與版本無關的 delivery_reconciliation_ttl_seconds;它在版本 1 與版本 2 的行為完全相同,統一記載於 Commands 總覽。與 mode 一樣,不設定時它不會出現在儲存的定義裡。版本 2 的寫入型定義還接受選填的 effect_identity 區塊,宣告方式見下文,不設定時同樣不會出現。

版本 2 的 input 名稱必須符合 ^[a-z0-9_]{1,32}$(否則在 loc ["body","definition"] 回 422 version 2 input names must match ^[a-z0-9_]{1,32}$),且 input 與 step 名稱不能重複(Input and step namespaces must be distinct)。版本 1 接受任何 1–64 字元的名稱,其 $input. placeholder 也接受中日韓字元,所以含大寫、中日韓字元或超過 32 字元的名稱,會讓 v1→v2 遷移還沒改到 step 就先在 input 名稱上失敗。

七種 step kind

kind用途綁定
select從資料表、既有 relation 或字面值讀出一個有界 relation$rel.<name>
let由運算式算出一個具名值$var.<name>
assert條件為 false 時讓整次執行失敗無
insert寫入新列;搭配 on_conflict 時,更新或保留已佔用該 unique_tuple 鍵的列$rel.<name>(它的 returning 對映)
update寫入匹配到的列$rel.<name>
delete刪除匹配到的列$rel.<name>
set_legal_hold透過 invoker-authority、write-mode Command 管理資料表法律保全無

最多 40 個 steps,其中 insert、update、delete 合計最多 20 個;超過時會在 loc ["body","definition"] 回 422(version 2 allows at most 40 total steps/version 2 allows at most 20 write steps)。Step 名稱唯一且符合 ^[a-z0-9_]{1,32}$。

select、assert、insert、update、delete 可以帶 when 守衛;let 與 set_legal_hold 不行。在 select 與 assert 上,守衛不為 true(false 或 null)時該 step 標記為 skipped:被略過的 select 綁定空 relation,被略過的 assert 不綁定任何東西。在資料列寫入型 step 上,守衛會針對每一次 for_each 迭代評估,且可以讀取 $item;只有在一次迭代都沒執行時,該 step 才回報 skipped,並綁定空的 returning relation。

set_legal_hold 具有 name、table、嚴格布林 active,以及須含非空白字元、長度 1–512 的 reason。執行者當下須具資料表管理權,最多 20 個保全動作,不可混用 insert/update/delete 或 child_collection step(順序不拘,save 與 delete 皆然),不可宣告 effect_identity,也不是資料列提案。保護銷毀的界線見保留期限與證據。

select

{ "kind": "select", "name": "lines", "from": { "table": "Order lines", "as": "l" }, "joins": [ { "type": "inner", "table": "Orders", "as": "o", "on": { "link": { "source": "$row.l.Order", "target": "$row.o.id" } } } ], "select": { "line_id": { "$metadata": { "source": "l", "name": "id" } }, "sku": "$row.l.SKU", "qty": "$row.l.Qty" }, "where": { "$eq": [{ "$metadata": { "source": "o", "name": "id" } }, "$input.order_id"] }, "order_by": [{ "expr": "$row.l.SKU", "direction": "asc" }], "max_rows": 500 }

這裡的 Order 是原生 link 欄位,所以 join 使用 link 形式。link 與 id 之間的純量 eq join 在儲存時會以 422 invalid_expression(comparison requires compatible operand types)拒絕;{"and": [...]} 只用在 stored 純量鍵,例如存放 id 的字串欄位。

子句契約
fromtable、relation("$rel.<name>")、values(有界的具型別陣列運算式)三選一,加上必填的 as 別名與選填的 filter。
joins最多 4 個。每個帶 type(inner | left | right)、table / relation / values 三選一、唯一的 as 別名、選填 filter 與 on。
on{"and": [ {left, op:"eq", right} … ]} 形式的 1–8 個 key,或 link join {"link": {"source": …, "target": …}}。兩側都是 $row.<alias>.<field> 參照。
select1–100 組「別名 → 運算式」。這些別名就是該 relation 的欄位名稱。
where / having布林運算式。
distinct布林值。
group_by最多 20 個運算式。
order_by最多 20 組 {expr, direction},direction 為 asc(預設)或 desc。
limit選填,1–1000。
max_rows必填,1–1000。

來源別名與 join 別名在同一個 step 內必須唯一。

select 的形狀錯誤屬於 request 驗證——是 loc ["body","definition","steps",<i>,"v2","select",…] 底下的 FastAPI 422 清單,而不是 program error 信封:join requires exactly one table, relation, or values source、join key must be a $row.<alias>.<field> reference、link join key must be a $row.<alias>.<field> reference、select source and join aliases must be unique,以及 values source expression cannot be null。由於 on 與 from 接受好幾種形式,清單裡還會針對每一種沒對上的形式各帶一筆 Field required 或 Extra inputs are not permitted。格式正確、但與資料表對不上的 join,則是 author 階段的 422 join_validation_failed 信封——例如 Native link join requires link-to-id operands、Native link join must connect a bound link column to the joined table id、Native link join does not match the authoritative link definition、Scalar join keys require stored scalar or metadata operands 或 Every scalar join key must connect the newly joined source to a previously bound source。

select 的來源與每一個 join 目標,執行者(authority: definition 的 Command 則看它的 table_access ceiling,read: none 一樣失敗)都必須讀得到。只要其中一張解析出的 can_read 是 none,整次呼叫就以 403 Read access not granted for this table. 失敗——與 insert、edit 的閘門一樣大聲。2026-07-31 之前,row ACL 會把讀不到的資料表變成永不匹配的條件:/query 回 200 加一個空 relation,agent 回報「查無符合資料」;在寫入程式裡,「先查再寫」的去重步驟查不到東西,就寫進一筆重複資料。這道閘門只管讀取——只寫不讀的 insert 目標本來就可以是 can_read: "none" 搭配 can_insert: true,而 dependency contract 帶進來的 link 或 lookup 資料表仍以只有 id 的替身呈現,兩者都不會 403。

Select 絕不截斷。沒有宣告 limit 時,結果超過 max_rows 會讓該 step 失敗並回 400 {"error":"query_limit_exceeded","details":{"resource":"relation_rows","limit":…,"actual":…}}。設定 max_rows 是正確性決策,不是效能提示。宣告的 limit 大於 max_rows 是 authoring 錯誤(Select limit cannot exceed max_rows);而且只要宣告了 limit,在所有模式下都必須同時宣告非空的 order_by(Select limit requires deterministic order_by),不是只有 query 模式才需要。

Typed SQL 綁定與 relation 值

已合併 CT #1962 保留 SQL 子句綁定及後續 select 來源的型別值。陣列、multi_select、link、attachment、JSON 與 object 使用 typed JSON 傳遞,而非直接交給 driver 的 list。日期保留 ISO 表示,decimal 項目使用精確 decimal wire 字串;既有 decimal 傳遞限制仍適用。投影陣列透過 relation 或 values 來源重用時保留完整內容,包含祖先集合與巢狀陣列。Relation 布林值仍為 boolean,integer 陣列不經 binary float 轉換,typed decimal 陣列結果保留 decimal wire 值。這不改變 input 或 let.type 的有界宣告文法。

$in、$not_in、$contains 與 $not_contains 的陣列形式使用完整值相等:結構化子集合不會匹配較大的 JSON 項目,"1" 這類字串也不會被轉成數字 1。Null needle 保持 unknown。SQL JSON 成員運算作用在已儲存的陣列型 cell 時,null 或非陣列 pool 也保持 unknown,包括否定運算;update/delete 條件不會選中這些 cell。對 scalar 常值清單,null 項目不會把有效但不匹配的 needle 變成匹配或 unknown。動態空集合可用;空陣列常值仍是型別化的 authoring 拒絕。

$datediff 參與 decimal SQL 算術時提供精確 scale-zero 係數,不經 float,保留整數天數邊界、負差值與 null 傳播。無法精確下推的計算型 decimal 陣列會以 author-phase 422 unsupported_clause_expression(Computed decimal arrays require a bounded projection)拒絕;這不是允許四捨五入或轉型不精確值。

自 backend 5.21.0 起,整數或 decimal 的 $single 可以成為後續 select 的 SQL 子句與投影中精確算術及比較的運算元:它以單一精確值綁定、保留宣告的 scale,不經 float。以前,對 $single 做算術,以及把它與這類算術或幣別/UOM 欄位比較,會被 422 unsupported_clause_expression 拒絕(與一般 decimal 欄位的單純比較本來就可以);真正不精確的混用仍然被拒絕。幣別或 UOM 欄位上的 $single 會保留單位,所以與不同單位比較或相加會以 ct.unit_mismatch 失敗(見Decimal 貨幣與計量單位),而 expr 是計算運算式、不是單一欄位的 dimensional $single,則因單位無法帶進 SQL 而以 author-phase 422 unsupported_clause_expression 拒絕。基數(400 cardinality_failed)與 null 行為不變。

純量陣列 input 與 $in

array input 可以宣告它的項目是單純的值。把 item_schema 設成一個純量型別名稱——string、integer、float、decimal、boolean、date 或 datetime——而不是物件項目用的欄位對應表:

{ "name": "item_ids", "type": "array", "required": true, "min_items": 1, "max_items": 50, "item_schema": "string" }

每個項目都必須是該型別的非 null 值(decimal 項目和物件項目一樣是純十進位字串),陣列上限照舊:min_items 與 max_items 介於 0 到 200,沒有設 max_items 時視為 200。在不是 array 的 input 上宣告純量 item_schema,儲存時會被拒絕,回 422(scalar item_schema applies only to array inputs)。這七種項目型別的陣列,只要和左邊運算元的型別相容,都可以當成 $in 的右運算元(用一串 string 比對 record id,用一串 integer 比對整數欄位,依此類推)。step 就是這樣用一串 record id 選出資料列:

{ "kind": "select", "name": "stock", "from": { "table": "Stock", "as": "s" }, "select": { "stock_id": "$row.s.id", "sku": "$row.s.SKU", "qty": "$row.s.Qty" }, "where": { "$in": ["$row.s.id", "$input.item_ids"] }, "max_rows": 50 }

這個 $in 條件也可以放在來源的 filter。清單會以帶型別的值傳進 SQL,所以比對遵循上面的整值規則:字串 "1" 不會等於數字 1,null 的比對值仍是 unknown。decimal 的成員比對是精確的;用計算出來的 decimal 去比對這種清單時,小數位數上限是 18 位,更寬的值會被拒絕,而不是被四捨五入。

成本。 引擎只讀 step 引用到的欄位,也就是它的 select、where、having、join、分組與排序裡出現的欄位,所以沒用到的欄位不會增加成本。用 id,或用 $in 給一串 id 篩選來源,是直接查找,成本不隨資料表大小增加;前提是這個 $in 就是整個 where 或 filter,或是最上層 $and 的其中一項。放在 $or、$not 底下、用 $not_in,或放在投影裡,就不走這條路徑。用其他欄位篩選,則必須逐筆查看資料列,成本會隨資料表變大。step 只投影的 link 欄位,是在篩選之後,針對留下來的資料列才去取。想讓檢查保持便宜,請讀由指令維護的儲存餘額,不要在檢查時才把流水加總;WMS 使用案例示範了這個做法。效能指南把這些做法收在一起。

let 與 assert

let 只計算一次,並把結果綁定為 $var.<name>。選填的 type 以三種形式之一宣告預期型別:純量名稱(null、string、text、integer、float、decimal、boolean、date、datetime、time、select、multi_select、link、attachment、json);物件 {"kind":"object","fields":{"<name>":"<純量>"}},1–100 個欄位,名稱符合 ^[a-z0-9_]{1,32}$;或陣列 {"kind":"array","items":<純量或物件型別>,"max_items":1–1000}——陣列不能巢狀。這裡的 decimal 不帶精度或小數位數。格式錯誤的型別是 FastAPI 422 清單。宣告在存檔時檢查,而且永遠不會轉型:integer 可以滿足 float,但 decimal 宣告會拒絕 integer、物件必須欄位完全相同,陣列的 max_items 也必須不小於值的上限。不相符時回 422 {"error":"invalid_expression","phase":"author","message":"Let expression does not match its declared type","path":"steps[<i>].type",…}。$json_get.type 使用同一套文法。

{ "kind": "let", "name": "unit_total", "value": { "$aggregate": { "relation": "$rel.lines", "expr": "$item.qty", "op": "sum" } } }

assert 是寫在程式裡的商業規則。它接受布林 condition、選填的 when 守衛,以及必填的 message(1–512 字元)。條件為 false 是 422 {"error":"assertion_failed",…},並讓整次執行回滾。選填的 "public": true 可讓 definer Command 的 execute-only 呼叫者看到失敗斷言的 message,見作者選擇公開的結果。

{ "kind": "assert", "name": "has_lines", "condition": { "$exists": { "relation": "$rel.lines" } }, "message": "That order has no lines to ship." }

寫入型 steps

{ "kind": "update", "name": "stamp", "table": "Orders", "match": { "id": "$input.order_id" }, "set": { "Status": "Shipped" }, "max_rows": 1, "returning": { "order_id": { "$metadata": { "source": "old", "name": "id" } } } }
insertupdatedelete
欄位載荷values(≤100 個 key)set(≤100 個 key)—
選列方式—match / where 恰好一個match / where 恰好一個
衝突處理選填的 on_conflict——
max_rows不接受1–1000,預設為 11–1000,預設為 1
returning 可用影像newold、newold

版本 2 寫入型 step 的 for_each 是指向有界陣列的參照字串——$input.<array>、$var.<陣列 let> 或 $rel.<先前的 relation>——而不是任意運算式(for_each must reference a bounded input, variable, or relation、for_each requires a typed bounded array)。物件列在該 step 內以 $item.<field> 取用,純量陣列則以 $item.value 取用。有 for_each 時,when 會針對每次迭代評估,且可以參照 $item。所有寫入型 step 的迭代上限加總必須不超過 1000,否則程式以 mutation_limit_exceeded(Command write-attempt expansion exceeds its limit)失敗。returning(≤100 個別名)會綁定一個具名 relation,供後續 steps 與 outputs 讀取——它就是版本 1「只能取 id 的 $steps 擷取」的替代品。

無法寫入目標欄位的值會在存檔時以 author 階段的 422 write_validation_failed 信封拒絕。結構檢查 Computed columns are read-only、Computed columns cannot be write match keys 與 Link writes require an authoritative link definition 的 path 是 steps[<i>].values|set|match.<送出時的 key>(用 step 的索引,不是名稱);值的檢查則帶被寫入運算式的路徑,steps[<i>].values|set.<內部欄位 key>:Expression cannot be written to the target column type、Null cannot be written to a required column、JSON object cannot be written; stringify or extract it、Typed array cannot be written to this column(只有 multi_select、link 與 attachment 接受陣列)、Typed array elements do not match the target column type、Typed array can exceed the target column cardinality、Principal cell literal must be user:<id>, smc:<id> or room:<id>(把不是已標記 principal cell 的字串常值寫入 principal 欄位時)以及 List expression is not bounded to one target item(寫入基數為一的 link 或單一 attachment 時)。json 欄位接受 JSON、物件、陣列、字串、數字、decimal、布林或 null。

寫入 step 可以用 {"$single": {"relation": "$rel.<select step>", "expr": "$item.<field>"}},把前一個 select 從另一張資料表的附件欄位讀到的檔案填進 attachment 欄位。對外部 client 或不帶使用者的通道而言,這種複製只有在 Command 作者宣告了 attachment_copies grant 時才會被接受(自後端 5.22.0 起);見附件複製。

在 values / set 內唯一可用的資料表別名是 current(僅 update 有),所以你不能用 $row.<step>.<alias> 拼出前一個 step 的列。請用 relation helper 讀取前一個寫入 step 的 returning relation——單列情境用 $single,其餘用 $aggregate / $count_rows / $exists。$collect 與 $first 需要有排序的 relation,而 returning relation 沒有排序。

以 on_conflict 做 upsert

自 backend 5.21.0 起,insert step 可以指定一條 unique_tuple 規則,並宣告「要寫入的那一列,其鍵已被佔用」時該怎麼處理。這裡沒有另外的 upsert 路由,也沒有第二套唯一性機制:step 沿用該規則自己的鍵,實際走的分支就是一般的 insert 或 update。

{ "kind": "insert", "name": "save", "table": "Daily counts", "values": { "Subject": "$input.subject", "Day": "$input.day", "Count": 1 }, "on_conflict": { "rule": "subject_day_key", "action": "update", "set": { "Count": { "$add": ["$existing.Count", 1] } } } }

範例假設 Daily counts 上有這樣的規則:{"type": "unique_tuple", "name": "subject_day_key", "components": [{"column": "Subject", "null_policy": "forbidden"}, {"column": "Day", "null_policy": "forbidden"}]},並宣告了輸出 "saved": {"relation": "$rel.save", "select": ["outcome", "record_id", "version"], "max_rows": 1}。

欄位約定
rule必填,1–128 字元:該 step 目標資料表上恰好一條已啟用 unique_tuple 規則的 id、name 或 constraint_id。其他情況(含舊式 unique 規則)都是 author 階段的 422 write_validation_failed(on_conflict requires one enabled unique_tuple rule on its target table,path 為 steps[<i>].on_conflict.rule)。
action必填:update、nothing 或 error。
setupdate 時必填,其他兩者則會被拒絕(request 驗證 422,Value error, on_conflict.set is required only for action update)。1–100 筆「欄位 → 運算式」,寫法同 update step 的 set;計算欄位(formula、lookup、rollup)是 422 write_validation_failed(Computed columns are read-only)。運算式以 $existing.<column> 讀取佔用該鍵的那一列;values 不能讀。

Step 會先組出它要 insert 的那一列——包含 input、預設值,以及每個 stored formula 欄位會得到的值——再由該列推導規則的鍵,並在鎖定下查詢這個鍵。因此 stored formula 可以作為鍵的 component,查詢找到的就是一般 insert 會撞上的那一列。由伺服器編號的 sequence 欄位要到 insert 本身才會編號,所以組鍵時它仍是空的;被 upsert 指名的規則不要以這種欄位當 component。

查詢結果updatenothingerror
沒有列佔用該鍵,或新列因某個 distinct component 為 null 而沒有鍵寫入新列:outcome 為 inserted相同相同
呼叫者讀得到的某一列佔用該鍵對該列套用 set:updated不寫入任何東西:unchanged409 unique_tuple_conflict,與一般 insert 相同
呼叫者讀不到的某一列佔用該鍵409 unique_tuple_conflict,body 只有 error 與 message(Unique constraint conflict),即使每個 component 都讀得到也不帶 constraint 或 columns;不寫入任何東西,也不回傳 receipt相同任何 insert 都會有的一般 409 unique_tuple_conflict,可能點出 constraint 與讀得到的 columns

新列中 forbidden 的 null 會先以規則自己的 400 拒絕(Rule '<name>' violated: a required unique tuple component is null)。action: "error" 時,這個 step 就是一般的 insert。

Receipt。 不論走哪個分支,step 的 returning relation 除了你自己的別名,還固定帶三個保留欄位:outcome(inserted、updated 或 unchanged)、record_id,以及 step 之後該列的 version。relation output 是清單,所以範例回傳 {"saved": [{"outcome": "updated", "record_id": "eeeeeeee-5555-4555-8555-eeeeeeeeeeee", "version": 2}]}。你自己的別名在每個分支都以 $new.<column> 讀取最終的那一列。在 returning 下為 outcome、record_id 或 version 撰寫不同的運算式是 422 write_validation_failed(Upsert receipt field names are reserved);儲存的 step 在這些名稱下帶的是正規的 receipt 運算式,原樣存回會被接受。upsert step 自己最多可以有 97 個別名。若呼叫者讀不到結果列,例如 read filter 排除了剛寫入的那一列,relation 為空,不會洩漏 id 或 version。

讀取與權限。 $existing 遵循 select 的讀取規則:走到 update 分支時,set 若讀取了對呼叫者隱藏的欄位,會在任何寫入之前回 403 column_not_readable;不在呼叫者讀取權限內的列(row filter,或 definer Command 的 read ceiling)則是上述的一般衝突。insert 與 edit 權限針對實際走的分支檢查:呼叫者不可執行的 insert 或 update 回 403 Command write access not granted for this table.;edit 權限只涵蓋其他列、把既有那一列排除在外時回 403 Command write access not granted for this row.;unchanged 結果不寫入任何列,所以不需要(沒有任何列佔用該鍵時,nothing 會寫入新列,需要 insert 權限)。action: "error" 時,與一般 insert 一樣先檢查 insert 權限(403 Insert access not granted for this table.)。

其餘都是一般寫入。 分支走的是與 insert、update step 相同的核心:資料表規則、commands_only 寫入政策、SCP floor、history、新的 version、trigger,以及 effect identity 的行為都與那裡一致。鎖住該列的規則(immutable_when),或把該列移到另一個存活鍵的 set,會讓呼叫失敗且沒有任何效果(400 immutable_when_violation、409 unique_tuple_conflict);unchanged 不會寫入 version 或 history。相同鍵的併發呼叫會依序執行,因為每次執行都會鎖住它的資料表:第一個 insert,之後每個呼叫都 update。

審批。 require_approval 規則套用在實際走的分支。呼叫會暫存待審的變更並回 409 approval_required;被保留的是當下選定的操作,連同它的目標列與值,放行時不會切換、也不會重新評估。因此兩個併發的首次呼叫可能各自保留一筆 insert:兩者都核准時,先放行的套用,後放行的以 unique_tuple_conflict 結束於 apply_failed,且不寫入任何東西。再執行一次該 Command,就會保留 update。讀不到的衝突不會暫存任何東西,dry_run 預覽也永遠不會暫存待審項目。

儲存的形式。 儲存的 step 以 constraint_id 指名規則、以欄位 id 作為 set 的 key,並把 $existing.<column> 讀回為正規的 current 欄位節點。變更規則的 component 或選項會換掉它的 constraint,移除規則則會刪掉它,所以指名它的 Command 會回 409 schema_dependency_changed,直到你以 on_conflict.rule 用 name 或 id 指名該規則並重新儲存為止(見相依漂移時 fail closed)。GET 回傳的 constraint_id 已不再符合,所以把定義原樣存回會得到上方 rule 欄位的 422 write_validation_failed。在 IaC command 行中,rule 是規則名稱,set 使用欄位 ref。

參照節點與運算式詞彙

值文法由四種參照家族構成。

節點意義
$rel.<name>由 select step 或寫入 step 的 returning 綁定的 relation。
$row.<alias>.<field>綁定到某個來源或 join 別名的列的欄位。這是 authoring 期的語法糖。
{"$column": {"source": "<alias>", "id": "col_<hex>"}}編譯器儲存的正規化、抗改名欄位參照。
{"$metadata": {"source": "<alias>", "name": "<key>"}}紀錄 metadata:id、version、created_at、updated_at、sort_order、created_by、created_by_ai、created_by_client、table_id。

你寫 $row.l.SKU,讀回來是 {"$column": {"source": "l", "id": "col_…"}}——兩種拼法指的是同一個參照。$input.<name> 與 $ctx.<key> 的行為和版本 1完全相同,而 ACL row policy 的 $me / $me.department / $now / $today±Nd 一樣不屬於這套文法。

友善 token 必須恰好解析到一個對象。資料表 token(from.table、joins[].table、寫入 step 的 table、incoming rollup 的 source_table)依名稱或 id 同時符合 command scope 內多張存活資料表時,回 422 {"error":"ambiguous_reference","phase":"author","message":"Table reference is ambiguous; use @table:<id>","retryable":false,"details":{}}。欄位 token 同時符合顯示名稱、內部欄位 id 與 metadata key 其中兩種以上時——例如顯示名稱是 id 或 created_at 的欄位——訊息是 Column reference is ambiguous; use an explicit token。請改寫成 @table:<id>、@column:<col_id>(例如 $row.l.@column:col_…),或正規的 $column/$metadata 節點。在建立與更新時,這是唯一有自己 body 的資料表解析失敗;其他解析不到的資料表 token 一律回 404 Referenced table not found in this scope。

運算子涵蓋算術與比較($add、$sub、$mul、$div、$neg、$eq、$neq、$gt、$gte、$lt、$lte、$between、$in、$not_in、$contains、$not_contains、$and、$or、$not、$is_null、$is_not_null)、精確十進位($decimal、$quantize、$divide——見精確十進位)、字串($concat、$len、$lower、$upper、$to_string、$pad_start)、日期($today、$now、$year、$month、$datediff)、本地時間與日期($time_of_day、$local_date、$date_add、$time)、JSON($json_get、$json_parse、$json_stringify)、select 投影、having 或 order_by 中的分組彙總($sum、$avg、$min、$max、$count、$count_distinct),以及 $case。$literal、$reference、$json_literal 與 $array_literal 是編譯後的內部節點名稱,不是可撰寫的運算子:寫它們會得到 422 invalid_expression(unknown expression operator '$literal');在運算式位置寫空陣列 [] 也一樣(array literal must not be empty);作為 $json_get.from 或 $json_stringify 來源的 JSON 常值則可以是空的。請改寫成裸的 JSON 純量或陣列,或在 $json_get.from / $json_stringify 中使用一般物件。

Command 的 $today 在已設定時使用公司業務日界線;設定為 null 時沿用 UTC。這不會讓 Commands 支援 ACL 的 $today±Nd 語法。

另有六個 helper 讀取已綁定的 relation。在 expr 內,該 relation 的欄位以 $item.<field> 取用。

Helper形狀結果
$exists{"relation": "$rel.x"}boolean
$count_rows{"relation": "$rel.x"}integer
$aggregate{"relation", "expr", "op"},op ∈ count、count_distinct、sum、avg、min、max;decimal 的 avg 另外需要 result_scale(0–18)與 rounding_mode純量
$collect{"relation", "expr", "max_items"}(1–1000)陣列——需要有序的 relation
$first{"relation", "expr"}純量或 null——需要有序的 relation
$single{"relation", "expr"}純量;relation 不是恰好一列時回 400 {"error":"cardinality_failed","details":{"expected":1,"actual":N}}

$single 讓你用宣告式的方式主張「剛好一筆匹配」,不必寫在用戶端程式裡。注意寫入 step 的 returning relation 不是有序的,所以對它使用 $first 或 $collect 是 authoring 錯誤——需要有序語意時請在 select 上宣告 order_by。

$case

{"$case": [{"when": <布林>, "then": <值>}, …], "else": <值>}——else 是與 $case 並列的選填 key。分支依序嘗試,第一個 when 恰好為 true 的分支勝出(false 與 null 都會往下走),而且只會評估被選中的 then。都沒有符合時結果是 else,省略 else 時為 null。when 是版本 2 的布林運算式,例如 {"$gt": […]},不是版本 1 的 {column, op, value}。結果型別是所有分支與 else 的共同型別:integer 配 float 得到 float,只要有 decimal 就是 decimal,decimal 配 float 會得到 $case cannot mix float and decimal branches,其他組合則是 $case requires compatible branch types。形狀錯誤會在存檔時回 422 invalid_expression:$case contains unknown configuration、$case requires one or more branches、$case branches require when and then、$case when requires boolean。沒有額外的分支上限;由每個運算式的預算(深度 12、256 節點)限制。所有分支都能轉成 SQL 的 $case 可以出現在 where 中。

陣列型 $case 分支的項目型別必須相容。結果保留該項目型別、合併後的 nullability,以及最大分支 max_items;項目型別不相容是 author-phase invalid_expression($case requires compatible array item types)。

本地時間與日期

自 backend 5.21.0 起,有四個僅限 Command 的運算子,可把儲存的 UTC 時間點轉成業務當地的時間或日期,並以整天為單位移動日期。它們在文法中與 $today 並列。{"$now": []} 是伺服器的 UTC 時間點,精度到分鐘(YYYY-MM-DD HH:MM,型別 datetime),與 $today 一樣在整次執行中固定不變。

運算子形狀結果
$time_of_day[<datetime>, "<zone>"]、[<datetime>] 或 []time:當地時鐘,格式 HH:MM
$local_date[<datetime>, "<zone>"] 或 [<datetime>]date:當地的日曆日
$date_add[<date>, <整數天數>]date,依帶正負號的天數移動
$time"HH:MM"用於比較的 time 常值

時間點與時區。 $time_of_day 與 $local_date 的第一個運算元是 datetime(datetime 欄位或 input、$now,或像 "2026-10-06 20:59" 這樣的常值),並被視為 UTC 時間點;date、字串型運算式或其他任何東西,會在存檔時以 422 invalid_expression 拒絕($time_of_day requires a datetime instant)。時區放在第二個運算元:IANA 地名(例如 Asia/Taipei、America/New_York)、Etc/… 時區或 UTC,可以寫成常值,也可以取自任何字串型運算式,例如 input 或欄位。轉換依該時間點當下的 IANA 規則進行,與伺服器或其資料庫跑在哪個時區無關,所以因調整時鐘而被跳過的當地時刻不會出現,重複的時刻則會出現兩次。

拒絕情形。 常值時區在 command 儲存時檢查:offset(+08:00、UTC+8)、空字串、localtime、像 EST5EDT 這樣的裸舊式名稱、未知名稱或超過 64 字元的名稱,都是 422 invalid_expression(Timezone must be a valid IANA name; offsets are not accepted);不是字串型的時區則是 Timezone must be a typed string expression。執行期才讀到的時區每次使用都會檢查:無效或 null 的時區是 400 invalid_expression(An explicit, valid IANA timezone is required,phase: "execute"),並讓整次執行回滾。明確指定的時區絕不會退回 UTC 或公司時區。

不指定時區。 $time_of_day 搭配 [<datetime>] 或 [],以及 $local_date 搭配 [<datetime>],會使用公司業務日曆中的業務時區,未設定時用 UTC,且每次執行只解析一次;{"$time_of_day": []} 就是以這種方式轉換 $now。如果規則不該隨公司設定改變而移動,請用明確指定時區的寫法。$today 與 $now 維持不變。

null 與 $date_add。 null 的時間點、日期或天數會得到 null。$date_add 接受一個 date 與一個整數天數。datetime 運算元,或 float、boolean、字串的天數,會在存檔時以 422 invalid_expression 拒絕($date_add requires a date and integer days;不是純日期的字串常值則是 $date_add requires a date, not a datetime)。常值天數超過 ±366000 會在存檔時拒絕($date_add day count exceeds 366000 days);計算出的天數超過此範圍,或結果早於西元 1 年、晚於 9999 年,則是 422 numeric_overflow,phase: "execute"。

time 型別。 時間常值要寫成 {"$time": "05:00"}:兩位數字、冒號、兩位數字,範圍 00:00 到 23:59。24:00、5:00、05:00:00 與非字串都是 422 invalid_expression($time requires a strict HH:MM literal)。time 只能與另一個 time 或 null 以 $eq、$neq、$lt、$lte、$gt、$gte、$between 比較;$in 與 $not_in 可對由 {"$time": …} 常值組成的陣列使用,$is_null 與 $is_not_null 也可用。與字串、date、datetime 或數字比較會在存檔時被拒絕(comparison requires compatible operand types;$in、$not_in 則是 <operator> requires compatible member type),也沒有算術。裸的 "05:00" 是字串,所以要寫成 {"$time": "05:00"}。let 可以宣告 "type": "time",step 的 schema 回報型別 time,值則是字串 HH:MM。沒有任何欄位型別是 time,所以 time 不能寫入欄位(Expression cannot be written to the target column type):請拿它做比較、當分支條件,或放進 output 回傳。

適用範圍。 可用於 when、assert、let、$case、outputs、select 的投影,以及寫入的 values、set 與 returning;沒有任何運算元讀取欄位時,也可用於 SQL 子句:只含 $now、input、變數與常值時,結果每次執行只計算一次,並以常數綁入查詢。作用在欄位上時,它們在 update、delete 的 where、select 的 from.filter 與 joins[].filter、join 鍵、group_by、having、order_by,以及分組、彙總或 distinct 的投影中會以 422 unsupported_clause_expression 拒絕。公式、資料表規則、view filter 與 read filter 不接受這些運算子。

{ "kind": "let", "name": "business_day", "type": "date", "value": { "$case": [ { "when": { "$lt": [{ "$time_of_day": ["$input.instant", "Asia/Taipei"] }, { "$time": "05:00" }] }, "then": { "$date_add": [{ "$local_date": ["$input.instant", "Asia/Taipei"] }, -1] } } ], "else": { "$local_date": ["$input.instant", "Asia/Taipei"] } } }

搭配 datetime 型 input instant,這段會把台北時間 05:00 之前的時間點歸到前一個當地日。在 select step 中,where 也可以比較資料列欄位的當地時間值:

{ "kind": "select", "name": "early_punches", "from": { "table": "Punches", "as": "p" }, "select": { "code": "$row.p.Code" }, "where": { "$lt": [{ "$time_of_day": ["$row.p.Instant", "Asia/Taipei"] }, { "$time": "05:00" }] }, "order_by": [{ "expr": "$row.p.Code", "direction": "asc" }], "max_rows": 50 }

這種判斷式會在有界 runtime 中逐列執行,所以 SQL 不會依這個 where 縮小範圍:step 會讀取來源、來源的 filter 與 join 提供給呼叫者的每一列,並計入既有的每個 select 20 000 列、每次執行 50 000 列的工作列上限(400 query_limit_exceeded,details.resource 為 work_rows_per_select 或 work_rows_total)。它在 limit 與 max_rows 之前過濾:limit 依 order_by 順序保留最先通過的列,通過的列數超過 max_rows 則是 400 query_limit_exceeded(relation_rows)。where 內也可以放一般的欄位比較、$case、$and、$or、$not、null 檢查、算術、$year、$month 與 $datediff;其中任何其他僅限 runtime 的運算子,例如作用在欄位上的 $to_string 或 $decimal,都是 422 unsupported_clause_expression(Bounded select predicates support stored temporal operands only)。select 必須沒有分組、沒有 distinct,也沒有 having 或彙總投影(Bounded temporal predicates require an ungrouped, nondistinct scalar select);在當地時間運算子內部、或在 $case 中與它並列的 SQL 彙總同樣會被拒絕(SQL aggregates cannot feed runtime local temporal expressions)。

分組彙總

運算子運算元結果
$sum數值($sum requires a numeric operand)運算元的型別,可為 null
$avg數值;decimal 運算元需要三運算元形式 [value, result_scale, "<mode>"]float,decimal 運算元則為 decimal;可為 null。非 decimal 卻給三個運算元時為 average policy applies only to decimal operands
$min、$max數值、string/text/select、date/datetime($min requires an ordered operand)運算元的型別,可為 null
$count、$count_distinct任何型別integer,永不為 null

每個都只收一個運算元,可以直接寫({"$sum": "$row.l.Qty"})或寫成單一元素的陣列。運算元必須是涵蓋該 step 來源、可轉成 SQL 的純量列運算式——常值、$input、$var 或只能在 runtime 計算的值會得到 $count requires a SQL-scalar row operand。在 select 中,它們屬於投影、having 與 order_by;在 where、filter、join 條件與鍵,以及 group_by 中會是 422 unsupported_clause_expression。在 select 之外沒有可彙總的列,所以存檔時就會被拒絕。這也包括 update 的 set 與寫入 step 的 returning:這類運算式裡任何位置出現彙總——包在其他運算子底下、在 $case 的條件、分支或 else 中(即使是永遠不會執行的分支),或在沒有比對到任何列的 update 中——都會是 422 unsupported_clause_expression,帶 phase: "author",path 指出該運算式或有問題的運算元。insert 的 values 完全沒有綁定任何列來源,所以那裡的彙總或行內 computed 運算元會更早以 422 invalid_expression 失敗。當整個值本身就是彙總時,message 是 Expression cannot be lowered in this clause(path 是整個值);當同一個值中有只能在 runtime 執行的部分——$quantize、$divide 這類運算子(或作用在非常數運算元上的 $decimal/$to_string)、已儲存的計算欄位,或行內的 $rollup、$lookup、$formula——使整體不算彙總時,則是 SQL aggregates and inline computed operands are not supported in write values or returning,其 path 指的是最外層的彙總子樹(或該行內 computed 運算元)。請先在較早的 select step 算出數字,再用 $single 或 $aggregate 這類 relation helper 讀回來。有 group_by 時,其他投影都必須出現在 group_by 中(Every non-aggregate projection must appear in group_by);沒有 group_by 時,全域彙總不能與列投影混用(A global aggregate cannot mix with row projections)。

在這項檢查之前就已儲存、且帶有這類運算元的 Command 不會被改寫,也無法再執行:每次執行都會重新編譯它儲存的 definition,回 422 unsupported_clause_expression(phase: "author"),欄位或 rule 變更讓它變成過期之後則回 409 schema_dependency_changed。在它還是最新的期間,對它所引用資料表的 trigger 編輯會被 409 {"error": "command_dependency_refresh_failed", "message": "a dependent Custom Table command cannot accept the candidate triggers"} 拒絕。請重新撰寫或刪除它。

內嵌計算值

$lookup、$rollup 與 $formula 會在運算式中直接計算某一列的計算值,每個都必須是其物件中唯一的 key:

  • {"$lookup": {"record", "link_field", "target_column"}}
  • {"$rollup": {"record", "direction", "link_field", "aggregation", "target_column", "source_table", "match", "filter", "result_scale", "rounding_mode"}}——direction 是 outgoing 或 incoming,incoming rollup 必須提供 source_table
  • {"$formula": {"record", "expression"}}

record 是該運算式中已綁定之存活資料表別名的列 id——"$row.<alias>.id" 或 {"$metadata":{"source":"<alias>","name":"id"}};relation 別名不算(Inline computed record source is not a live table)。其餘設定由與儲存型計算欄位相同的驗證器檢查,失敗時回 422 invalid_expression,path 指出欄位。$lookup 走 many link 時回傳最多 1000 個值的陣列;$rollup 的計數回 integer、decimal 目標回 decimal、加總與平均回 float;$formula 回傳公式產生的值。三者都可為 null,而且只能在 runtime 計算:出現在 where、filter、join 鍵、group_by、having、order_by,或分組、彙總、distinct 投影中時,會是 422 unsupported_clause_expression;出現在 update 的 set 或寫入 step 的 returning 運算式的任何位置也一樣(SQL aggregates and inline computed operands are not supported in write values or returning;在沒有綁定列來源的 insert values 中則是 422 invalid_expression)——請在 select step 中算出來,再從它的 relation 讀回結果。它們的 link 目標與公式來源會加入 command 的資料表集合,所以必須在 command 的 tag 之下。執行時以呼叫者的權限(definer Command 則是它的 ceiling)、每批最多 1000 筆紀錄計算(relation_limit_exceeded);逾時回 504 query_timeout,其他資料庫失敗回 500 command_execution_failed。

JSON 運算子

運算子形狀結果
$json_parse{"value": <文字>, "on_error": …}——兩個 key 都必填json
$json_get{"from": <JSON 來源>, "pointer": "<RFC 6901,≤2048 字元>", "type": <宣告型別>, "on_error": …}——四個都必填宣告的 type
$json_stringify<來源> 或 {"value": <來源>}文字:key 排序、緊湊分隔符號且保留非 ASCII 的正規 JSON;來源為 null 時輸出文字 null

on_error 是 "fail"、"null" 或 {"default": <符合型別的值>}。設為 "null" 時失敗會得到 null,設為預設值時得到該值,設為 "fail" 時則回 400 {"error":"json_parse_failed"|"json_pointer_failed","phase":"execute","message":"JSON value does not satisfy the declared expression contract","retryable":false,"details":{},…}。$json_parse 在運算元不是文字、輸入超過 262 144 個 UTF-8 bytes、JSON 無效、含 NaN/Infinity,或解析後深度超過 32 層、超過 8192 節點時失敗。$json_get 在 pointer 解析不到(空 pointer 代表整份文件;陣列步驟必須是數字,- 會被拒絕)或值不符合 type 時失敗;型別為 json 的結果(或 object、array 型別裡的 json 欄位)還必須在 32 層、8192 節點以內,取出值的根算第 0 層——來源本身可能更大,例如 json 儲存格或 input,因為 input 最多可達深度 64、16 384 節點。$json_stringify 沒有 on_error:超過同樣深度與節點上限,或含 NaN/Infinity 的值,一律回 400 json_parse_failed,訊息為 JSON value could not be stringified。寫在來源位置的 JSON 常值會在存檔時依同樣上限檢查(422 invalid_expression,JSON depth or node limit exceeded)。形狀錯誤也是 422 invalid_expression——$json_parse requires value and on_error、$json_get requires from, pointer, type, and on_error、on_error must be fail, null, or a typed default、$json_get requires a JSON source。除了常數折疊之外,這些運算子只在有界 runtime 中執行,所以在 SQL 子句中會是 422 unsupported_clause_expression。

精確十進位

decimal 值——decimal 欄位、decimal input 或十進位運算式——一律以純十進位表示的 JSON 字串傳遞,絕不會是浮點數或指數形式。

{ "inputs": [ { "name": "unit_price", "type": "decimal", "precision": 12, "scale": 2, "allow_negative": false, "required": true, "nullable": false }, { "name": "qty", "type": "integer", "required": true, "nullable": false } ], "steps": [ { "kind": "let", "name": "line_total", "value": { "$quantize": [{ "$mul": ["$input.unit_price", "$input.qty"] }, 2, "half_even"] } } ] }
語法契約
decimal input需要 precision(1–38)、scale(0–min(18, precision))與 allow_negative。值是像 "12.50" 這樣的純十進位字串;JSON 數字、指數、多出的小數位數或不允許的負號,都會回 400 input '<name>': invalid decimal value(不做任何捨入)。
{"$decimal": "12.50"}由文字運算元建立 decimal($decimal requires a decimal string;無法解析的常值是 $decimal literal is invalid)。
$add、$sub、$mul、$neg精確運算——絕不捨入。整數會提升為 decimal;與 float 混用則是 float and decimal operands cannot be mixed。
$divdecimal 不能用:decimal division requires $divide with result_scale and mode。
{"$quantize": [value, scale, mode]}以 0–18 的常值 scale 與明確的 mode 捨入:half_even、half_up(中間值遠離零)、down(朝零)、floor 或 ceiling。value 必須是 decimal($quantize requires decimal operands);scale 不是常值或超出範圍是 $quantize requires a literal scale from 0 to 18,缺少或未知的 mode 是 $quantize requires an explicit rounding mode。
{"$divide": [a, b, scale, mode]}精確除法後依 scale 與 mode 捨入;除以零是 numeric_invalid。a 與 b 必須是 decimal($divide requires decimal operands),scale 與 mode 的拒絕訊息與 $quantize 相同。
分組 $avg在 decimal 欄位上的形式是 {"$avg": [value, result_scale, "<mode>"]};單一運算元的寫法會得到 decimal average requires explicit result_scale and rounding_mode。$sum、$min、$max 仍只有一個運算元。
$aggregate 的 avg對 decimal 運算式需要 result_scale 與 rounding_mode(decimal $aggregate avg requires result_scale and rounding_mode);用在其他地方則是 rounding options apply only to decimal $aggregate avg。sum 是精確的、min/max 以數值比較,count_distinct 把 1.5 與 1.50 算成一個。

比較以數值進行、不看小數位數(1.5 等於 1.50)。整數可以和 decimal 比較;float 永遠不行(comparison requires compatible operand types)。字串常值只有作為比較運算元,或 $in / $contains 的探針時才會轉成 decimal——以字串常值清單比對 decimal 會被拒絕,請寫成 [{"$decimal": "1.50"}, …]。在 $case 中,decimal 與整數分支得到 decimal,decimal 與 float 分支則是 $case cannot mix float and decimal branches;decimal 分支中請寫 {"$decimal": "0"} 而不是 0。

在 values / set 中寫入 decimal 欄位需要十進位運算式——整數、float,或不是純十進位字串的字串常值,在儲存時會以 422 write_validation_failed(Expression cannot be written to the target column type)拒絕。是純十進位字串的字串常值("12.50"、"-3";不可有指數、+ 號、多餘的前導零("0.5" 可以)、單獨的 .5 或 5.、空白或負零)會原樣被接受,{"$decimal": "12.50"}、decimal input 與它們的算術結果也一樣。儲存時只檢查常值的格式:資料列路徑會以欄位的小數位數儲存,多出的位數會被拒絕而不是捨入,所以把 "12.505" 寫入小數位數 2 的欄位可以存檔,但寫入資料列時會失敗。

回傳的值:欄位參照保留欄位的小數位數("1.50");$decimal、算術與加總會去掉尾端零("1.50" + 3 → "4.5");$quantize、$divide 與 decimal 平均值則剛好以指定的小數位數呈現。

$quantize、$divide,以及作用在欄位或其他執行期值上的 $decimal(作用在常值、$input、$var 或 $ctx 值上時是單一綁定值,任何子句都能用)只能在有界 runtime 中執行,所以在 SQL 子句中——where、filter、join 鍵、group_by、having、order_by、distinct——會以 422 unsupported_clause_expression(Expression cannot be lowered in this clause;在 select step 的 where 中訊息是 Bounded select predicates support stored temporal operands only)拒絕。下推到 SQL 的十進位算術使用最多 65 位的精確整數係數;伺服器無法精確下推的運算會以 unsupported_clause_expression、phase: "author"(HTTP 422)拒絕,即使是在 execute 或 query 時才發現。無法精確帶入 SQL 的 decimal 值則是執行期的形式:400 unsupported_clause_expression,phase: "execute"——Decimal SQL bound value cannot be represented exactly at 18 fractional places(綁定到 SQL 子句的執行期 decimal 超過 18 位小數,或絕對值達 10^47 以上)、SQL array parameter is not a list,以及這類 decimal 經由 relation 流入後續 select 時的 Decimal relation value cannot be transported exactly as DECIMAL(65,18)(或 as VARCHAR(130))。

Typed JSON 傳遞也可能回傳 400 unsupported_clause_expression、phase execute,訊息為 SQL array parameter cannot be transported as typed JSON、Select SQL JSON transport is unsupported 或 Command SQL JSON transport is unsupported。Native SQL JSON 傳遞失敗只做窄範圍映射;其他資料庫失敗仍是已清理的 500 command_execution_failed,不回傳原始 SQL、參數或資料庫 exception 文字。既有精確 decimal overflow 拒絕仍獨立保留。

有兩個封包錯誤碼只屬於 decimal,都是 422,帶 phase: "execute"、retryable: false 與 details: {}:

errormessage何時發生
numeric_invalidDecimal expression is invalid不是純十進位字串的執行期值進入 $decimal、$quantize、$divide、十進位算術或彙總,或 $divide 除以零
numeric_overflowDecimal expression exceeded its bound 或 Exact decimal SQL coefficient exceeds the 65-digit domain有界 runtime 中係數超過 128 位,或 SQL 路徑上超過 65 位。溢位的列會排在最前面,所以 limit 或分頁都藏不住它們

$to_string 與 $pad_start(v5.10.11)

新增兩個字串運算子,讓 command 可以自己產生格式化的識別碼——案件編號、流水號、補零代碼——不必再繞回用戶端程式。

運算子元數契約
$to_string1把整數、浮點數、decimal、布林、date、datetime、字串、text 或 select 值算成字串(decimal 會精確地以一般記數法呈現並保留欄位的小數位數,例如 1.50;浮點數維持浮點數的形式,例如 2.5、2.0 或 1e+16)。清單、link、attachment、json 與 principal 在建立時就會以 $to_string requires an integer, float, boolean, date, datetime, or string operand 被拒絕(這段文字早於它也接受的 decimal、text 與 select 運算元)。null 仍是 null。
$pad_start3[value, width, fill]。用單一字元 fill 把 value 左補到 width 個字元。width 是 0–64 的整數,fill 必須剛好一個字元。

算出來的字串是決定性的,所有 runtime 路徑共用同一套:整數變成十進位數字、浮點數變成最短可還原形式(2.5、2.0、1e+16)、布林是 true/false、date 與 datetime 是 ISO 8601、字串維持原樣。

字面 width 不在 0–64,或字面 fill 不是剛好一個字元,都是建立時錯誤。非字面的 width/fill 改在執行時做範圍檢查,超出範圍時回 null,所以此時結果型別是可為 null 的。

這兩個運算子都沒有 SQL 下推。運算元全為常數時會摺成一個 bound parameter;否則必須在有界的 runtime 執行,因此子句矩陣會在建立時就拒絕把它們放進 where 之類的 SQL 子句,而不是等到下推時才失敗。

整數型別的 SQL 投影會保持整數。整數欄位是以 NUMERIC(65, 30) 下推,所以 MAX/SUM 會回傳 decimal——宣告為 integer 的 let 過去會拿到 2.0,$to_string 就把第二個流水號算成 CASE-2026-02.0。現在投影型別為 integer 的值會被正規化回整數;float 型別與沒有型別的值維持原本的呈現。

內建輸入範例(v5.10.11)

definition.examples 最多可放三筆 {name, inputs} 實際呼叫範例,與指令一起編寫:

"examples": [ { "name": "一般出貨", "inputs": { "customer": "長榮工業", "item": "螺絲 M4", "quantity": 12 } } ]

每一筆在建立時都會用編譯後的 inputs 驗證——與 /execute 用的是同一套——失敗時回 400 {"error":"command_example_invalid","example":…,"index":…,"message":…}。因此範例不可能與它所附的定義脫節。

第一筆範例會被寫進聊天室 agent 的 tool description,以精確的 tool 參數 JSON 呈現(「請完全照這個形狀複製」),並且會附在 input 遭拒的回應裡,讓模型的修正呼叫一次到位,而不是再猜一次。對於陣列型 input,一個具體的呼叫形狀勝過一段文字描述。

與 mode 一樣,這個 key 在未設定時會省略,所以沒有範例的定義維持位元相同,IaC diff 與 dependency contract digest 都不會變動。JSONL IaC 的 v1→v2 匯出升級也會把 examples 帶過去。

Outputs 是值離開的唯一出口

版本 2 的 execute 回應只報告 step 的metadata——name、kind、status(succeeded 或 skipped)、schema 與 row_count。steps 裡沒有任何列。呼叫者需要的每一個值都必須宣告。

"outputs": { "shipment_id": { "value": { "$single": { "relation": "$rel.shipment", "expr": "$item.shipment_id" } }, "description": "Id of the shipment row this run created." }, "lines": { "relation": "$rel.lines", "select": ["line_id", "sku", "qty"], "max_rows": 100, "description": "The order lines covered by this shipment." } }

最多 100 個 outputs,key 符合 ^[a-z0-9_]{1,32}$。純量 output 是 {"value": <運算式>};relation output 是 {"relation": "$rel.<step>", "select": [別名…], "max_rows": 1..100}。兩者都接受選填的 description,長度上限 1024 字元;純量 output 還可以設定 "public": true(見作者選擇公開的結果)。名稱 approval_provenance 保留給 kernel 擁有的審批 provenance:撰寫同名 output 會以 422 command_output_reserved 拒絕。

description 不是裝飾。每一個宣告的 output 都會被寫進 agent 工具說明的 Outputs: 區塊——每個 output 一行 - <名稱>: <說明>,沒寫說明時則是單純的 - <名稱>——讓模型讀得到 output 的意義,而不是從 key 去推。過去 output 名稱只會以回應 body 的 key 形式抵達模型。這個欄位預設為 null,未設定時完全不輸出,所以既有定義與 IaC 文件的來回轉換仍然位元組穩定。

Relation output 的實際上限是 min(100, max_rows) 列,整個 outputs 物件則上限 65 536 bytes。超過是 400 {"error":"output_limit_exceeded","details":{"resource":"output_rows"|"output_bytes",…}}。Command 不是批次匯出 API——每次執行 100 列、64 KB 就是硬上限。

Step 回報的 schema 中,每個欄位都帶一個來自封閉 21 值清單的 type——string、text、select、integer、float、decimal、boolean、date、datetime、time、user、social_client、principal、coordinate、multi_select、link、attachment、json、object、array、null——外加 nullable 旗標。這讓你能直接依 command 產生用戶端程式碼而不必猜。每個 step 最多回報 100 個欄位,row_count 範圍 0–1000。

完整範例:出貨一筆訂單

讀出訂單明細、拒絕空訂單、以加總數量建立出貨單、蓋章訂單,最後交回出貨單 id 與出貨明細。

{ "name": "fulfil_order", "description": "原子性地出貨一筆訂單:建立出貨單並更新訂單狀態。", "tag_id": "55555555-5555-4555-8555-555555555555", "definition": { "version": 2, "inputs": [ { "name": "order_id", "type": "string", "required": true, "nullable": false, "max_length": 36 }, { "name": "carrier", "type": "string", "required": true, "nullable": false, "max_length": 64 } ], "steps": [ { "kind": "select", "name": "lines", "from": { "table": "Order lines", "as": "l" }, "joins": [ { "type": "inner", "table": "Orders", "as": "o", "on": { "link": { "source": "$row.l.Order", "target": "$row.o.id" } } } ], "select": { "line_id": { "$metadata": { "source": "l", "name": "id" } }, "sku": "$row.l.SKU", "qty": "$row.l.Qty" }, "where": { "$eq": [{ "$metadata": { "source": "o", "name": "id" } }, "$input.order_id"] }, "order_by": [{ "expr": "$row.l.SKU", "direction": "asc" }], "max_rows": 500 }, { "kind": "assert", "name": "has_lines", "condition": { "$exists": { "relation": "$rel.lines" } }, "message": "That order has no lines to ship." }, { "kind": "insert", "name": "shipment", "table": "Shipments", "values": { "Order": "$input.order_id", "Carrier": "$input.carrier", "Units": { "$aggregate": { "relation": "$rel.lines", "expr": "$item.qty", "op": "sum" } }, "Created by": "$ctx.user_id" }, "returning": { "shipment_id": { "$metadata": { "source": "new", "name": "id" } } } }, { "kind": "update", "name": "stamp", "table": "Orders", "match": { "id": "$input.order_id" }, "set": { "Status": "Shipped" }, "max_rows": 1 } ], "outputs": { "shipment_id": { "value": { "$single": { "relation": "$rel.shipment", "expr": "$item.shipment_id" } } }, "lines": { "relation": "$rel.lines", "select": ["line_id", "sku", "qty"], "max_rows": 100 } } } }

執行它只會回傳 metadata 與宣告的 outputs:

{ "execution_id": "bbbbbbbb-2222-4222-8222-bbbbbbbbbbbb", "status": "succeeded", "executed_at": "2026-07-21T03:02:11Z", "steps": [ { "name": "lines", "kind": "select", "status": "succeeded", "schema": { "line_id": { "type": "string", "nullable": false }, "sku": { "type": "string", "nullable": true }, "qty": { "type": "integer", "nullable": true } }, "row_count": 2 }, { "name": "has_lines", "kind": "assert", "status": "succeeded", "schema": {}, "row_count": 0 }, { "name": "shipment", "kind": "insert", "status": "succeeded", "schema": { "shipment_id": { "type": "string", "nullable": false } }, "row_count": 1 }, { "name": "stamp", "kind": "update", "status": "succeeded", "schema": {}, "row_count": 1 } ], "outputs": { "shipment_id": "cccccccc-3333-4333-8333-cccccccccccc", "lines": [ { "line_id": "dddddddd-4444-4444-8444-dddddddddddd", "sku": "WIDGET-1", "qty": 2 }, { "line_id": "eeeeeeee-5555-4555-8555-eeeeeeeeeeee", "sku": "WIDGET-2", "qty": 1 } ] } }

executed_at 凍結在原始執行的時間,所以重播會回相同的時間戳。回應會丟掉頂層值為 null 的 key,因此請把「key 不存在」視為 null;每個 step 仍一定帶 schema,沒有回傳 relation 的 step 則是空的({})。

版本 2 的 rules

rules 陣列的用法與版本 1 相同——最多 20 條具名 rule、phase 為 before_execute 或 before_commit、message 必填 1–512 字元——而且 before_execute 的 rule 仍然只能參照 $input 與 $ctx。

失敗封套不同。版本 1 的 rule 失敗是 422 {"error":"command_rule_failed","rule":"<name>","message":…};版本 2 則是 program error 封套:422 {"error":"command_rule_failed","phase":"execute","step":null,"path":null,"message":"<rule message>","retryable":false,"details":{}}——body 裡沒有 rule 名稱。請對 error 分支,不要對 detail.rule 分支。

宣告 effect identity

版本 2 的寫入型 command 可以宣告一次執行所記錄的是哪一個業務事件,讓同一個事件再次發生時回傳原本的 receipt,而不是再寫一次。本節是宣告方式;事件重複時回傳什麼,請見執行語意。這項能力是 ct.effect_identity major 1,會列在能力探索裡。沒有這個區塊的 command,行為和以前完全相同。

{ "version": 2, "inputs": [ { "name": "receipt_no", "type": "string", "required": true, "nullable": false, "max_length": 64 } ], "steps": [ { "kind": "select", "name": "receipt", "from": { "table": "Goods receipts", "as": "r" }, "select": { "receipt_id": { "$metadata": { "source": "r", "name": "id" } }, "qty": "$row.r.Qty" }, "where": { "$eq": ["$row.r.Receipt", "$input.receipt_no"] }, "max_rows": 1 }, { "kind": "insert", "name": "movement", "table": "Stock movements", "values": { "Qty": { "$single": { "relation": "$rel.receipt", "expr": "$item.qty" } } }, "returning": { "movement_id": { "$metadata": { "source": "new", "name": "id" } } } } ], "outputs": { "movement": { "relation": "$rel.movement", "select": ["movement_id"], "max_rows": 1 } }, "effect_identity": { "namespace": "erp.inventory", "kind": "goods_receipt_post", "key": [ { "$single": { "relation": "$rel.receipt", "expr": "$item.receipt_id" } }, "gr" ] } }

effect_identity 與 steps 並列在定義裡,可用於 POST 與 PUT .../commands、POST .../commands/validate,以及 JSONL IaC 的 command line。與 mode、examples 一樣,不設定時它不會出現在儲存的定義裡,所以沒有它的定義仍然逐位元組相同,IaC diff 與 dependency-contract 摘要也都不會變動。

欄位契約
namespace必填。符合 ^[a-z][a-z0-9_.-]{0,63}$ 的小寫 slug,代表業務領域,例如 erp.inventory。
kind必填。同樣的格式;代表 namespace 內的事件種類,例如 goods_receipt_post。
key必填。1–8 個有順序的 key term,其中至少一個是針對資料列 id 的 row term。

一次執行所記錄的 occurrence 是 (company, namespace, kind, key)。同一個 key 換成別的 namespace 或 kind,或出現在另一家公司,就是不同的事件。key term 有兩種形式:

Term契約
Literal1–256 字元且不以 $ 開頭的字串、絕對值不超過 2^53 − 1 的整數,或布林值。Literal 有型別:true、"true" 與 1 是三個不同的 term。
Row term必須恰好是 {"$single": {"relation": "$rel.<step>", "expr": "$item.<field>"}},或同樣的物件改用 $first,不能有其他 key。

Row term 讀取的是 command 自己所選資料列的不可變 metadata,而不是呼叫者提供的值。這正是設計重點:人、agent 或 trigger 可以選擇要對哪一筆既有資料列動作,卻無法靠輸入新的 key 來創造新事件。

  • <step> 是位於所有 insert、update、delete step 之前的 select step,所以 key 永遠不會依賴 command 自己的寫入,事件在任何寫入之前就已決定。它不能使用 group_by。
  • <field> 是該 step 的一個 projection,而且這個 projection 必須是綁在資料表 source(from.table 或 joins[].table,不是 relation 或 values source)alias 上、純粹的 {"$metadata": {"source": "<alias>", "name": "id"}}(或 "table_id")。速記寫法 $row.<alias>.id(或 $row.<alias>.table_id)會被正規化成該節點,可以使用。欄位值、version、updated_at、created_by 與其他所有 metadata 名稱都會被拒絕,因為它們在資料列建立之後仍可能改變。
  • $single 要求該 step 剛好回傳一列(否則是原本的 400 cardinality_failed)。$first 取第一列,並要求該 select 有 order_by;選取結果為空、或被 when 略過時,會在任何寫入之前以 422 effect_identity_unbound 拒絕。
  • 詞彙是封閉的。key 裡不可使用 $input、$ctx、$var、$today、$row、其他運算子與以 $ 開頭的 literal。$input 有專屬訊息:穩定的外部 id 輸入需要 service-principal 的呼叫者,而平台目前沒有這種呼叫者。

靜態規則在定義存檔時就會強制檢查,使用的是建立、更新、validate 與 IaC apply 共用的 compiler。違規會回 422 與 program-error 封套,phase 為 author,path 對 term 是 effect_identity.key[<i>](區塊層級的規則則是 effect_identity.key 或 effect_identity);不會儲存任何東西:

{ "detail": { "error": "effect_identity_invalid", "phase": "author", "path": "effect_identity.key[0]", "message": "effect_identity key terms cannot reference $input: stable external-id inputs require a service-principal invoker, which this build does not support", "retryable": false, "details": {} } }

在 POST .../commands/validate 的報告裡,同樣的內容是 errors[] 的一筆,status 為 422。只有區塊本身的形狀錯誤——namespace 或 kind 不符格式、key 沒有 term 或超過八個、區塊內有未知 key、literal 型別不對,或字串 literal 是空的或超過 256 字元——才是一般的 request-validation 422 項目,位於 loc ["body","definition","effect_identity",…]。message 的值如下:

message原因
effect_identity requires a version-2 write Command版本 1,或 mode: "query"
effect_identity key terms cannot reference $input: stable external-id inputs require a service-principal invoker, which this build does not support$input.… 字串,或 row term 的 expr 中含 $input
Key literals cannot start with '$'; row values come from {'$single'|'$first': {relation, expr}} terms其他以 $ 開頭的字串 literal
Integer key literals must be within +/-(2^53-1)超出該範圍的整數
A row key term is exactly {'$single'|'$first': {'relation': '$rel.<select step>', 'expr': '$item.<field>'}}物件含其他 key,或使用其他 helper
<helper> key terms take exactly relation and expr、A row key term reads a select step: relation '$rel.<step>'、A row key term projects one field: expr '$item.<field>'relation 或 expr 格式不對
Key term references unknown step '<step>'relation 指到不存在的 step
Key term step '<step>' must be a select step (a write step's own rows cannot key an occurrence)該 step 不是 select
Key select step '<step>' must precede every insert/update/delete step該 select 位在第一個寫入之後
Key select step '<step>' cannot use group_by有分組的 select
Key field '<field>' of step '<step>' must be a bare row metadata reference (id or table_id); column values are editable and cannot key a permanent occurrenceprojection 是欄位、運算式或計算值
Key field '<field>' reads metadata '<name>'; only id and table_id are immutable for the life of a row任何其他 metadata 名稱
Key field '<field>' must read a table source, not a relation or values source僅為防禦性檢查;relation 或 values 的 alias 會更早失敗:$row.<alias>.<field> 會回上面的 bare-metadata 訊息(relation 沒有該欄位時是 422 invalid_expression Unknown relation field reference),明確寫 $metadata 節點則是 422 invalid_expression Unknown metadata source alias
$first over step '<step>' needs that select's deterministic order_by對沒有排序的 select 使用 $first
effect_identity key needs at least one row term on metadata id (an occurrence is anchored to a selected row)只有 literal,或只有 table_id 的 term

編輯已經記錄過事件的 command

替既有的 command 加上這個區塊是允許的,而且只對未來生效:先前的執行不會回填,所以區塊生效後的第一次執行會把該事件再記錄一次。一旦 command 已記錄至少一個事件,任何會變更 namespace、kind、literal term,或各 row term 所讀資料表與 metadata,或是移除區塊的 PUT(或 IaC apply)都會被拒絕,因為重新命名 key 會悄悄重新開啟所有已記錄的事件:

{ "detail": { "error": "effect_identity_locked", "phase": "author", "step": null, "path": "effect_identity", "message": "This Command has recorded business events; its effect identity (namespace, kind, key terms and their source rows) cannot change or be removed", "retryable": false, "details": {} } }

狀態碼是 409。其他所有編輯——說明、inputs、steps、rules、outputs——不受這個鎖影響。程式改變不算換 key:針對已記錄事件、以改過的程式重複執行,是 conflict,不是新事件。軟刪除 command 會保留它已記錄的事件,所以 identity 與程式都相同的另一個 command 仍會 replay 它們。

相依漂移時 fail closed

這是最要緊的操作差異。版本 2 的 command 在每次 execute 與每次 query 都會重新驗證整份 dependency contract。因此,任何尚未解決的 schema、rules、審批、trigger、改名、軟刪除、scope 或 tag drift 都會產生 409 schema_dependency_changed,直到作者以 PUT 重新存檔。

支援的 server mutation 以兩種不同政策避免無謂 drift。一般 column/rule 維護(包含 IaC、purge survivor 與 root cross-reference repair)只選 mutation 前 contract 仍 valid 的 commands,並在同一 commit 刷新 selected 且仍可編譯者。原本 stale 的 command 保持 stale。若 mutation 使 command 失效,mutation 優先:compile、closure escape 或 concurrent-edit failure 會略過該 command、讓 mutation 提交,command 則維持 fail-closed。非資料庫 prepare/refresh failure 也同樣降級;DB lock/deadlock 仍可能中止或重試 transaction。Multi-table purge/repair 依 rewritten table 自己的 scope 選取;這種分組是完整的,因為 dependency contract 採 exact-scope-closed:每個 closure member 都必須通過 command 的精確 scope filter,跨 scope authoring 則以 A referenced table does not exist in this scope 失敗。每個 participating scope 都會收到完整的 rewritten-table set。另一方面,透過 no-dependent fast path 併發建立的新 command,仍可能依 mutation 前形狀編譯並落成 stale。

Trigger edit 更嚴格。支援的 REST trigger PUT 或 IaC trigger apply 會先驗證 candidate graph,並要求所有 selected commands 都刷新成功,不限 DSL version。Trigger、history、refreshed definitions 與適用的 IaC state 以原子方式提交;任何 compile/closure/refresh failure 都 veto trigger mutation,REST 回 409 與 detail.error: "command_dependency_refresh_failed"。兩種政策都不會修復編輯前已 stale 的 command。見執行語意。

延伸閱讀

布林運算與 null(v5.10.0)

$and、$or 在執行時計算與 SQL 查詢編譯採相同的三值邏輯:false AND null 為 false,true OR null 為 true;沒有決定性布林值時,null 運算元讓結果維持未知。既非布林也非 null 的值同樣維持未知,不會被視為 truthy。

Execution column 的 value_type 也包含 user、social_client、principal 與 decimal,與 compiler 接受的 scalar types 一致;呈現 query/execution 結果欄位時請保留這些型別。

Last updated on