unique_tuple
unique_tuple 是 unique 的選用、較嚴格版本。它要求 1 到 16 個欄位組成的有序 tuple 不得重複,並把 unique 默認的每一個選擇都改成明確設定:null 的 component 代表什麼、文字是否不分大小寫比較,以及 Unicode 文字是否先做 NFC 正規化。適合「每家公司、每個會計年度的發票號碼不可重複」這類自然鍵——缺少某個 component 時不應悄悄豁免該列。
Payload
{
"type": "unique_tuple",
"name": "每家公司每年度發票號碼不可重複",
"components": [
{ "column": "公司", "null_policy": "forbidden" },
{ "column": "會計年度", "null_policy": "forbidden" },
{ "column": "發票號碼", "null_policy": "equal" }
],
"case_insensitive": true,
"unicode_normalization": "NFC"
}| 欄位 | 契約 |
|---|---|
components | 必填。1 到 16 個有序的 {column, null_policy} 物件。column 可用顯示名稱或內部 col_<hex> 鍵;同一欄位只能出現一次。 |
null_policy | 每個 component 都必填:forbidden、equal 或 distinct(見下文)。 |
case_insensitive | 選填的 JSON boolean,預設 false。對 string、text、select component 套用 Unicode case folding。 |
unicode_normalization | 選填,none(預設)或 NFC。對 string、text、select component 套用,且在 case folding 之前進行。 |
id、name | 選填,與其他規則相同。回傳的 constraint_id 可以原樣送回,伺服器會忽略它。 |
規則或 component 上的未知鍵,以及 null/[] 以外的 when,都會被拒絕。Shape 錯誤(0 或 17 個 component、缺少或未知的 null_policy、非 boolean 的 case_insensitive、像 columns 這樣多餘的鍵)是 422 request 驗證錯誤;語意錯誤則是 400,detail 為純字串:
detail | 原因 |
|---|---|
column '<ref>' not found in '<table>' | 欄位不存在(後面附候選提示) |
duplicate component '<column>' | 同一欄位出現兩次 |
unique_tuple link components require cardinality-one links | Link component 的 cardinality 是 many |
column '<column>' is not a supported stored scalar component | json、lookup、rollup、live(virtual)formula、multi_select、interval、attachment、coordinate 或 principal_list 欄位 |
a table takes at most 5 unique rules | unique 與 unique_tuple 共用每張表五條的上限 |
unique rules do not take a when pre-condition (SQL-enforced; unconditional) | 帶了非空的 when |
可用的 component 是 stored 的 string、text、integer、float、decimal、boolean、date、datetime、select、user、social_client、principal 欄位,以及 cardinality 為 one 的 link。GET 會以顯示名稱回傳 components[].column,並附上伺服器的 constraint_id。
formula_version: 2 且 computation_mode: "stored" 的 formula 欄位也算在這些 stored scalar 之內,因為 kernel 會把結果實體化(見 formula v2);live(virtual)公式則不算,使用它就會得到上面的 400。
鍵如何組成
每個 component 都會變成帶型別的值,所以 string 欄位的 "1" 永遠不會等於 integer 欄位的 1:
| 欄位型別 | 標準值 |
|---|---|
string、text、select | 文字本身;unicode_normalization 為 NFC 時先做 NFC 正規化,case_insensitive 為 true 時再做 case folding。空字串是真正的值,不是 null。 |
integer、float | 精確的十進位表示(1 ≡ 1.0,-0 ≡ 0) |
decimal | 去掉小數尾端零的標準十進位(1.2 ≡ 1.20) |
boolean | true / false |
link(cardinality one) | 唯一的目標紀錄 id |
date、datetime、user、social_client、principal | 儲存的文字,精確比較(不做 folding) |
在 case_insensitive 與 NFC 下,Straße 會與 STRASSE 碰撞,預組字 é 也會與 e 加上組合重音碰撞。鍵是對帶型別 tuple 計算的 SHA-256。
Null 政策
當 cell 缺值或為 JSON null,或 link 沒有目標時,該 component 視為 null。
null_policy | null 的 component 會… |
|---|---|
forbidden | 拒絕寫入:400 Rule '<name>' violated: a required unique tuple component is null。即使另一個 distinct component 也是 null,每次寫入仍會檢查所有 forbidden component。 |
equal | 本身就是一個值:其他 component 相同、且此 component 都是 null 的兩列會碰撞。 |
distinct | 豁免整列:不記錄任何 claim,永遠不會碰撞。 |
無法編碼的已儲存資料(不是純十進位字串的 decimal cell、boolean 欄位裡的非 boolean 值、帶多個目標的 link)會以 400 Rule '<name>' violated: … 拒絕並說明問題,例如 a unique tuple link contains multiple targets。
衝突
Create、update、revert 與 restore 會認領該列的鍵;delete 則釋放它。發生衝突的寫入回 409,body 絕不包含衝突紀錄的 id 或任何 tuple 值:
{
"detail": {
"error": "unique_tuple_conflict",
"constraint": "每家公司每年度發票號碼不可重複",
"columns": ["公司", "會計年度", "發票號碼"]
}
}重複時 error 為 unique_tuple_conflict;不同的 tuple 恰好得到相同雜湊時為 unique_tuple_hash_collision;兩個併發交易搶同一個鍵、資料庫索引拒絕落敗者時為 unique_tuple_claim_unavailable。這與舊式 unique 不同,後者的 409 只有在衝突列位於呼叫者的可讀範圍時才會附上 conflicting_record_id 與 key_tuple(否則省略這兩個欄位與隱藏欄位的名稱;自後端 5.21.0 起,只要有 component 對呼叫者隱藏,也會省略)——解析 unique 衝突的程式碼必須兩種形狀都能處理(例如時段產生器只容忍舊式形狀)。
自後端 5.21.0 起,上面的 body 絕不會點出呼叫者讀不到的欄位。當有 component 對呼叫者隱藏時,columns 只列出讀得到的 component,constraint 改成規則不透明的 rule_<hex> ID,而不是作者寫的名稱(名稱可能直接拼出隱藏欄位),並以 message: "Unique constraint conflict" 標示這是被遮蔽的回應:
{
"detail": {
"error": "unique_tuple_conflict",
"columns": ["公司", "發票號碼"],
"constraint": "rule_3f9a1c2b7d4e",
"message": "Unique constraint conflict"
}
}當沒有任何 component 讀得到,或無法為寫入者解析出讀取範圍時,body 只剩 error 與 message。三種 error 值的遮蔽方式相同。要判斷回應是否被遮蔽,請檢查 message,不要檢查 constraint。
Bulk insert、匯入與 publish session 會逐列預檢,並以 Unique tuple rule '<name>' conflict、… hash collision、… conflict within batch 或 … hash collision within batch 回報各列。對有 component 被隱藏的呼叫者,標籤是規則的 rule_<hex> ID;沒有任何 component 讀得到時則沒有標籤(Unique tuple rule conflict)。競爭同一個鍵的併發同步 POST .../records/bulk 請求只會有一個勝出,每個落敗者得到帶型別的 409;非同步 bulk 任務與匯入則如上所述逐列回報衝突。
v2 Command 的 insert step 可以在 on_conflict 指定一條已啟用的規則,並透過該規則的鍵找到目標列,以更新既有的那一列或不動它,而不是以上述衝突失敗;見以 on_conflict 做 upsert。當 component 是 stored formula 欄位時,該查詢編碼的是一般 insert 會儲存的那個值,所以重複呼叫會找到既有的那一列,而不會漏掉該筆 claim。由伺服器編號的 sequence 欄位要到 insert 本身才會編號,所以不能成為這種查詢所用鍵的一部分。
啟用與修改規則
約束由規則的 id 與其設定識別——包含各 component 的順序與 null 政策、case_insensitive 與 unicode_normalization。只改規則名稱會保留約束;改動上述任何一項(或 id)則會在 PUT 內丟棄舊的 claim,並依所有 live 列重建,全有或全無。既有資料重複時,PUT 會以同樣不含值的 409 unique_tuple_conflict 拒絕(沒有樣本列);既有列有 forbidden 的 null 時則回上述 400;兩種情況都不會儲存任何東西。rules.preview 只驗證 shape,從不執行這個回填。
相關欄位的保護
| 操作 | 回應 |
|---|---|
重新命名 component select 欄位的選項 | 400 option rename is protected by a unique_tuple rule; remove the rule first |
| 變更 component 欄位的型別 | 400 column type is protected by a unique_tuple rule; remove the rule first |
| 刪除 component 欄位 | 409,衝突訊息 Write rule '<name>' references this column ('<column>'); delete the rule first |
| 永久清除 link component 的目標表 | Purge 預覽的 blocker {"code": "unique_tuple_link_dependency", "table_id": "<來源表>", "message": "remove the dependent unique tuple rule before purging its link target"} |
核准
require_approval 的 create 或 upsert 以單一 component 的 unique_tuple 所涵蓋的欄位比對時,待審變更的鍵會使用該 tuple 自己的正規化——case folding、NFC 與 null 政策——所以 tuple 會視為同一個鍵的兩個請求,會以 409 approval_pending 碰撞。null_policy: "equal" 時,空的比對值也會碰撞;forbidden 時,null 的比對值會被拒絕。套用已核准變更時若發現 tuple 衝突,該變更會以 apply_failed 結束。unique_tuple 永遠不會被用來挑選以 match_column 比對的資料列 upsert 的目標。由規則的鍵挑選目標的地方有兩處:Command insert step 的 on_conflict,以及採用宣告鍵的匯入 v2(遷移匯入,或帶 key_policy: "declared_unique" 的對應),它透過宣告的 unique 或 unique_tuple 規則找到該列。
原生 v2 Command 若含核心管理的終態載體,只有 enabled、matching 核准規則能在 prescan 延後該 component,不建立暫時 claim;其他 forbidden component 仍拒絕。放行時注入真正終態事實後,原子檢查完整 tuple。失敗時 Review 保持 approved、hold 為 apply_failed,不提交業務/history/claim 效果,失敗收據不含值。Proposal staging 不能代替原生終態放行。已合併 #1996 只允許與來源證據無關的既有列 update:不可寫入或改變 protected carrier、approval mapping/condition 或 unique-key dependency。其他 terminal-target 異動仍以 409 approval_provenance_unavailable 拒絕;preparation 與 frozen release 再檢查此邊界。
請把規則放進完整的 rules 清單,透過 rules.set 發布。