資料表規則
規則是在資料進入自訂資料表之前執行的伺服器端契約。前端驗證可以改善操作體驗,但不能取代規則:callback、批次作業、觸發器與核准後重播都可能走不同寫入入口,最後仍由伺服器決定資料是否可落地。
先建立正確的心智模型
一張表的 rules 是一份完整、排序過的清單。PUT 會整份取代,而不是增量新增;送出前應先 GET、保留既有規則與穩定的 rule_<hex> ID、在本地修改,再預覽及回寫。每張表最多 10 條規則(超過會回 422)。
GET 現有規則 → 本地修改完整清單 → POST preview → PUT 完整清單 → GET 驗證Warning 送出只有一條規則的
PUT,會移除清單中其他規則。伺服器只會替沒有id的新規則產生 ID;編輯時請原樣保留既有 ID。
詳細 wire contract 請查閱 rules.get、rules.preview 與 rules.set。
18 種規則如何選
| 類型 | 回答的問題 | 頁面 |
|---|---|---|
compare | 同一列的兩個欄位是否維持大小或相等關係? | compare |
unique | 一至三個欄位組成的鍵是否不重複? | unique |
unique_tuple | 1 到 16 個欄位組成的有序 tuple 是否不重複,且每個欄位都明訂 null 政策,並可選擇 casefold/NFC 文字比對? | unique_tuple |
no_overlap | 同一範圍內的時間或數值區間是否不重疊? | no_overlap |
count_limit | 一個 live-row bucket 是否維持在設定上限內? | count_limit |
aggregate_limit | 有效群組的精確總和或筆數是否立即、或於 Command 最終狀態滿足界限? | aggregate_limit |
exists | 另一張表是否至少有一列符合關聯條件? | exists |
not_exists | 另一張表是否沒有任何列符合關聯條件? | not_exists |
same_dimension_reference | 這一列所連結的紀錄,是否與這一列屬於同一個必填維度紀錄(同一專案、同一公司、同一倉庫)? | same_dimension_reference |
date_interval_exclusion | 在由一或兩個 link 決定的分割內,日期期間 [start, end)(可選擇沒有終點)是否彼此不重疊? | date_interval_exclusion |
period_fence | 列的業務日期是否位於開放期間,或 Command 是否帶有已授權的覆寫? | period_fence |
transition | 狀態是否只沿允許的路徑改變? | transition |
check | 單一欄位值是否符合比較、集合或 regex 條件? | check |
require | 條件成立時,欄位是否一定有值? | require |
require_approval | 寫入是否應先暫存並送 Review 模組? | require_approval |
channel | 部門表的列是否落在呼叫者解析出的 channel scope(授權他這張表的所有房間之聯集)內? | channel |
invariant | 不論寫入者與入口,列的最終形狀是否都合法? | invariant |
immutable_when | 列一旦進入這個狀態,指定欄位是否就凍結? | immutable_when |
一般 predicate 規則可用 when 縮小套用範圍。unique、unique_tuple、same_dimension_reference、date_interval_exclusion、period_fence、transition、channel、invariant 與 immutable_when 不接受有效的 when;需要條件式 SCP 時,請把 if 節點寫在 policy 內,鎖定條件則寫在 immutable_when 自己的 prior_when,它以列的 pre-image 而非 post-image 判斷。
預覽再發布
Preview 一律會驗證候選清單;有上限的 row-impact pass 則只處理 channel/invariant,回報各 room 的 SCP 差異與 invariant 違規。它不會把 no_overlap、count_limit、exists、check 等一般 predicate rules 套到既有列上;這些 bucket 必須另以針對性的 record query 稽核。它也從不回填 unique/unique_tuple 約束,所以既有資料的碰撞只會在 PUT 的 409 出現。唯一的例外是 same_dimension_reference:preview 會執行與 PUT 相同的既有列准入檢查(在交易範圍的 guard 下進行,之後回滾),並回相同的 409 same_dimension_reference_violation。Preview 不寫入規則、不修改資料,也會忽略 expected_rules_digest。
curl -X POST \
"$BASE_URL/private/module/custom_tables/chatroom/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/rules/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"type": "compare",
"name": "實付不得超過報價",
"left": "實付金額",
"op": "lte",
"right": "報價金額"
}
]
}'{
"table_id": "22222222-2222-4222-8222-222222222222",
"sampled_rows": 120,
"total_rows": 120,
"sample_cap": 200,
"rooms": [],
"invariant_violations": 0,
"warnings": []
}把 sampled_rows 與 total_rows 一起顯示;若前者較小,UI 應標示「抽樣結果」,不要呈現成全表保證。SCP 預覽還會提供房間別結果與 invariant 違規數。
發布完整清單
curl -X PUT \
"$BASE_URL/private/module/custom_tables/chatroom/11111111-1111-4111-8111-111111111111/tables/22222222-2222-4222-8222-222222222222/rules" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @rules.json成功後,以回應中的正規化清單與新的 rules_digest 更新前端狀態;欄位名稱可能已轉成伺服器的可重送表示。常見失敗:
| 狀態 | 意義 |
|---|---|
400 | 規則無效(純字串 detail,例如 rules[0]: invalid rule type 'x'. valid types: …) |
409 rules_changed | expected_rules_digest 與已儲存的清單不符——請重新載入後再套用你的修改 |
409 approval_rule_locked | 仍有待審的 staged change 時修改 require_approval 規則 |
409 unique 碰撞 | 既有資料在新的 unique/unique_tuple 規則下已經重複;不會儲存任何東西 |
409 same_dimension_reference_violation | 既有資料已經違反新的 same_dimension_reference 規則 |
409 date_interval_exclusion_invalid_period / date_interval_exclusion_overlap | 既有資料已經違反新的 date_interval_exclusion 規則(不合法的期間,或同一分割內重疊的兩個期間) |
409 date_interval_exclusion_retry | 儲存在等待資料表的 guard 時,規則所指名的欄位或 link 被改變(或不再符合該規則);沒有儲存任何東西,請重新讀取 schema 後再送出請求 |
422 | Request 驗證失敗:超過 10 條規則;unique_tuple、same_dimension_reference、date_interval_exclusion、channel 或 invariant 的 shape 錯誤(Value error, rules[<i>]: …);channel/invariant policy 超過深度、leaf 或 link leaf 上限;或 expected_rules_digest 不是 64 位小寫 hex |
423 | 資料表正在 schema 作業而被鎖定 |
403/404 | Moderator 閘門:不是 moderator 的呼叫者回 Insufficient permissions. …;不存在或在垃圾桶的 id、(自後端 5.21.0 起)其他公司的資料表回 CustomTable Not found。在部門、公司或聊天室路徑上,你是 moderator 但屬於其他範圍的資料表(聊天室路徑上則是屬於其他聊天室的資料表)回 Table not found in this department/… in this company/… in this chatroom。聊天室已被刪除的聊天室資料表——資料表會比聊天室活得久——在每一種路徑上,對自己公司的呼叫者都回 404 Chatroom Not found,時間點在 moderator 檢查之前(其他公司的資料表如上所述回 CustomTable Not found) |
409 lock/500 | Lock 等待逾時或死結回 flat 的 409 Concurrent write conflict (lock); please retry the request.;其他資料庫錯誤回 500 Database operation failed.;其他非預期錯誤回 400 Failed to update rules: <text>。這些情況都不會儲存任何東西 |
防止覆寫他人的修改
GET 與 PUT 都會回傳 rules_digest,也就是對已儲存的正規化清單計算的 64 位 hex SHA-256。下次 PUT 時把它放進 expected_rules_digest:若期間有其他編輯者改了規則,寫入會以 409 {"detail": {"error": "rules_changed", "message": "Table rules changed; reload before saving."}} 拒絕,規則與 history 都不會寫入。省略此欄位(或送 null)則維持原本的無條件取代。這個摘要在候選清單驗證通過、取得資料表鎖之後才比對,早於 approval 鎖檢查與任何 unique 回填;把沒有變動的 GET 回傳重新存檔,摘要不會改變。
部門表含 enforcing channel 規則時,非管理員不再需要指名房間:floor 由「授權他這張表的所有房間」聯集解析。沒有東西可以重試,也沒有 candidates 可挑——見 union 契約。
與寫入錯誤整合
佇列中的 bulk insert/update worker 會在母表鎖內重新讀取目前規則。工作啟動後才提交的期間、筆數與區間規則仍會生效;進入佇列不代表規則快照被凍結。有序同步 bulk 也會在同一把鎖內更新規則 context。詳見批次操作。
一般 predicate 違規會拒絕寫入;前端應顯示伺服器訊息,同時保留使用者尚未送出的表單。每種規則各有自己的狀態碼與形狀;400 的文字前面會加上 Rule '<label>' violated: ,label 依序取規則的 name、id,最後才是 type:
| 種類 | 狀態 | Detail |
|---|---|---|
compare | 400 | '<left>' (<value>) must be <op> '<right>' (<value>) |
check | 400 | '<column>' (<value>) must match …、must be one of …、must not be one of … 或 must be <op> <expected>;超過 4096 字元的值是 … is too long to pattern-check |
require | 400 | '<column>' is required |
no_overlap | 400 | interval <start>..<end> overlaps record '<id>' sharing the same <scope columns>,或結束不晚於開始 |
exists/not_exists | 400 | no matching row exists in the required table/a matching row already exists (record '<id>') |
count_limit | 400 | limit of <max> matching rows reached |
immutable_when | 400 | '<columns>' cannot change while the row matches the lock condition … |
transition | 409,flat 字串 | Rule '<name or id>' violated: transition of '<column>' from <old> to <new> is not allowed(只在 update 與 revert) |
unique | 409 | {constraint, columns, conflicting_record_id, key_tuple};後兩者只在列可讀時出現;有 component 對呼叫者隱藏時會遮蔽成 {columns, constraint: "rule_<hex>", message} 或 {message} |
unique_tuple | 409 | {error: unique_tuple_conflict / unique_tuple_hash_collision / unique_tuple_claim_unavailable, constraint, columns},有 component 對呼叫者隱藏時以同樣方式遮蔽;違反 forbidden 的 null 則是 400 |
same_dimension_reference | 409 | {error: same_dimension_reference_violation, message: "Reference must share a live required dimension."} |
date_interval_exclusion | 409 | {error: date_interval_exclusion_invalid_period / date_interval_exclusion_overlap / date_interval_exclusion_retry, message}——不含任何值:不會有衝突的資料列、日期或 scope 值 |
channel/invariant | 403 | {error: scp_out_of_scope / scp_scope_undeclared / scp_invariant_violation, message, …} |
require_approval 的 409 approval_required 則不是失敗:它表示變更已被暫存,應切換到「待審」狀態。完整流程見核准流程指南。
若要互動式試走完整流程,可使用流程精靈。