record line
record 是依 natural key 比對的 seed/upsert line。它讓可重建的基礎資料跟系統宣告一起搬移,但刻意不寫 IaC state,也不支援刪除資料列。
JSONL Line
{"kind":"record","table":"orders","data":{"order_no":"A-100","status":"open","customer":["C-200"]},"on_drift":"skip"}欄位契約
| 欄位 | 必填 | 精確契約 |
|---|---|---|
kind | 是 | 固定 "record" |
table | 是 | target table ref |
data | 是 | 非空 object;keys 全是該 table column refs |
on_drift | 否 | "skip" 或 "update",預設 "skip" |
沒有 ref、spec、state 或 renamed_from。data values 仍遵守 column types。Link cell 使用目標 table natural-key values 的陣列;基數為 one 的 link 隨該行解析,指向文件稍後才宣告之目標資料列時,整行會延後到延後群組;基數為 many 的 cell 則在所有 records 可用之後解析(見原子行與延後群組)。
一份文件每張表最多可有 CUSTOM_TABLE_IAC_MAX_RECORDS 條 record line(預設 200);超過的那一條會以 record cap exceeded for table '{table}' ({max}) 中止 parse。Record line 不計入 line cap,但整份文件合計超過 CUSTOM_TABLE_IAC_SYNC_RECORD_LIMIT(預設 200)時,apply 會改為背景 ticket。
Ref 與身分規則
Record 沒有持久 IaC ref;工作台只用 {table}.record:{physical-line} 顯示該輸入行。真正 identity 是 table natural key 的值。Key 先取 table spec.key 的 column ref;若省略,改用位置預設,而 plan 與 apply 目前挑選的欄位不一定相同——請明確宣告 key(見自然鍵)。Server state 中既有 table/key refs 也可在 real plan 解析,但無 I/O 的工作台看不見它們。
data keys 必須是 authored column refs,不是 display names 或 col_<hex>。Natural key 必須包含值,不能是 boolean、json 或 interval 欄位,且在文件新 keys 與 live table 中都必須唯一。Principal(principal/user/social_client)欄位可以當自然鍵,因為 principal cell 就是一個純量字串。
Decimal cell 是字串
decimal cell 要寫成 JSON 字串,例如 "12.50";JSON 數字會被拒絕。每一行在分類之前,decimal cell 都會依 live 欄位驗證並正規化到欄位的 scale,因此 "12.5" 與已儲存的 "12.50" 視為相等,decimal 自然鍵也能找到既有資料列,而不是再插入一筆。欄位無法容納的值——小數位數多於 scale、位數超出 precision 允許範圍,或在 allow_negative 為 false 時的負值——是 validate-phase 錯誤(decimal column '<internal key>': …),會擋下整份文件。
Link cell 中的自然鍵清單不會正規化。若 link 目標的 key 是 decimal 欄位,每個 key 都要寫成儲存形式——scale 為 2 時寫 "1.20",不是 "1.2"——否則查不到資料列。
Principal cell 接受身分 token
user、social_client 或 principal 欄位的 cell 都可以填可攜身分 token 取代原始 ID——user 欄位填 $user:<username>,social_client 欄位填 $smc:<platform>:<platform_user_id>,principal 欄位可填 $user:、$smc: 或 $room:<聊天室名稱>(裸名重複時改用限定形式 $room:<部門名稱>/<聊天室名稱>)。Export 會輸出這些 token,這正是讓「指名人員與聊天室的種子資料」能跨安裝搬移的關鍵。
principal cell 存的是它的文法要求的帶標籤形式,所以 token 會解析成 user:<id>、smc:<id> 或 room:<id>——標籤就是 token 前綴去掉 $。$dept: 沒有對應的帶標籤 cell 形式,因此仍然只用於 grant:放進任何 cell 都是逐行的 kind 錯誤,principal token '$dept:Sales' does not match the column's type: a principal column expects $user:<...> or $smc:<...> or $room:<...> or a raw id。原始帶標籤 cell 仍然原樣接受,作為同環境的捷徑。
解析限定租戶,並在 record 分類之前逐 cell fail-closed:找不到或已刪除的使用者、活躍於多個聊天室的 social client,以及找不到或名稱模稜兩可的聊天室,每一種都是逐 cell 錯誤,絕不會靜默取第一筆。principal 型別的 cell 裡任何以 $ 開頭的值都會被當成 token,所以格式錯誤的那一個會回報自己的錯誤,而不是以假的原始 ID 流到寫入閘門。任何不以 $ 開頭的字串會原樣通過——user/social_client 欄位是原始 ID,principal 欄位是原始帶標籤 cell——同環境往返合法,但若打算搬到別處就不建議。
當目標表的自然鍵本身是 user、social_client 或 principal 欄位時,link cell 也一樣:export 會把自然鍵清單 token 化,plan 與 apply 都透過同一個共用 helper 解析。自然鍵是 principal 型別時,替換後用來查詢的值是帶標籤 cell——正是目標列實際儲存的值——所以 plan、apply 與 export 三方一致,重新匯出仍然 plan 出 0 差異。解析失敗的 token 會被排除在批次查詢之外,因此只會浮現解析器自己的錯誤,不會再多一則 no row … with key value '$room:…'。
生命週期與規劃
找不到相同 natural key 時 action 是 insert。找到後:declared cells 相同為 skip;不同時 on_drift: "skip" 保留 live row並回傳 warning,on_drift: "update" 規劃更新 declared cells。未宣告 cells 保留。decimal cell 以正規化形式比較,所以 "12.5" 等於保存的 "12.50"。
Link cell 永遠不算 drift:唯一差異是 link cell 的列會規劃 skip。新插入的列一定會寫入它的 link cell;既有的列只有在該行為 on_drift: "update"、且解析出的目標與 live 不同時才會重寫。何時寫入取決於 link 的基數——見原子行與延後群組。
Link cell 的目標表自然鍵,只取該表 table line 明確宣告的 key,或先前 apply 記進 state 的 key——絕不使用位置預設。因此沒有明確 key 的 link 目標表,在第一次 apply 時,指向它的每個 link cell 都會以 target table '{table}' has no resolvable key column 失敗:基數為 many 的 cell 會回報它、其列仍會寫入但缺了這個 link,基數為 one 的 cell 則讓整行被拒絕、什麼都不寫。之後的 apply 面對的是既有的列,所以只會替 on_drift: "update" 的行補上 link。請為每張 link 目標表宣告 key。失敗的 many link cell 以 ref: "record:{line}" 回報,各原因以 ; 串接(目標表無法解析,以及 cell 在寫入時被拒絕,則改帶 ref: "{table}.record:{line}"),而該 record 自己的結果仍是 applied: true,因為它的純量 cell 已經寫入;完整清單見錯誤目錄。
Record 不寫 state,重複 apply 依 key 收斂為 skip。IaC 不提供 record deletion;資料刪除必須走 records API 與它自己的授權/稽核流程。
原子行與延後群組
一行 record 對它寫入的資料列就是一個交易。與純量 cell 一起寫入的內容,取決於每個 link cell 的基數:
- 純量 cell 與每個基數為
one的 link cell 一起寫入,在資料表寫入 guard 之下以一次一般的 record 寫入完成,讓 kernel 以完整的資料列檢查 rule。該行不是整行 commit,就是被拒絕而什麼都不寫:它的結果是applied: false,apply 階段錯誤帶有原因(拒絕有 code 時也帶code)。 - 基數為
many的 link cell 在所有 record 行之後才寫入,每個 cell 一次寫入。被驗證或被date_interval_exclusionrule 拒絕的 cell,是該行自己的錯誤,該行其餘部分仍會套用(applied: true);這類 cell 的其他任何拒絕,則是在先前各行已 commit 之後讓整個 request 失敗:整個 request 會帶著該次寫入自己的 HTTP 錯誤失敗——write-policy 或 SCP 的403、審批暫停的409、lock 或 integrity 的409(Concurrent write conflict (lock); please retry the request.或Database integrity error.),或500——而以 ticket 執行的 apply 會以internal_error結束為failed。
基數為 one 的 link 若指向文件中後面某一行才會建立的 natural key——前向參照或選填的循環——就還無法解析,所以整行等待:什麼都不寫,由執行器保留給延後群組。在文件其他 record 行之後,群組會以一個交易、分多輪套用每一個等待中的行。constraint link(參照或期間分割 link)還無法解析的行會等到後續輪次;其他仍無法解析的單一 link,會在各輪結束後、同一交易內以第二次 update 寫入;而沒有任何進展的一輪,則會以各自的解析錯誤與 applied: false 拒絕剩下的行。這些行是在其他行之後才建立的,所以它們的建立順序、排序順序與版本都排在後面。永遠無法解析的基數為 one 的 link——目標列不存在,或目標資料表沒有 IaC 綁定(reference link target has no IaC table binding)——會讓它所在的整行被拒絕。Rule 違規絕不重試:kernel 拒絕該寫入、那一行立即失敗,群組整個回滾,並在不含它的情況下重新套用其餘行。
寫入的行會記錄一個版本與一個 trigger 事件(被略過或未變更的行都不記錄),但有一種例外:單一 link 指向群組在它之後才寫入之資料列的延後行,會在同一交易內以第二次 update 取得該 link,而每個基數為 many 的 cell 都是之後的一次 update。
Lock。 寫入 link cell 的行會以排他方式持有它所在資料表的寫入 lock,並以共享方式取得它經由 link 觸及的資料表(在延後群組內則是排他);任何落在帶有 date_interval_exclusion 或 same_dimension_reference rule 之資料表上的行,也會以排他方式持有該資料表,所以它會與該資料表的其他每個寫入者序列化。Lock 衝突會讓該行或該次群組嘗試被拒絕(MySQL lock 錯誤傳到 IaC 時帶 retryable_lock_conflict),該行等待期間若有 rule 編輯加入了新的 probe anchor(Table rules changed during write; retry)或 same-dimension reference 綁定(same_dimension_reference_violation)也一樣:請重新 plan 並 apply。
審批 gate。 延後行的寫入若會被 require_approval rule 攔住——它的建立或更新,或第二次 update——就會以 approval_staging_unavailable 被拒絕、且不暫存任何東西,因為群組在一個交易內套用、而暫存會 commit。等它的 link 目標都已存在之後再 apply 一次:該行就成為一次一般的寫入,gate 會重新判斷(rule 符合的寫入會被攔下送審,其他的則照常套用)。或者拆開 link 循環:先建列,再建 link。
受治理的資料表。 位於 same_dimension_reference 圖中、或帶有 date_interval_exclusion rule 的資料表走同一條路徑,其上設定 rule 敏感 link cell 的行,或完全沒有 link cell 的行,會在 rule 的 guard lock 之下以一次寫入完成;其他選填的基數為 many 的 cell 仍走全域流程。有效 write_policy 為 commands_only 的資料表,會在 plan 時以 governed_context_required 拒絕 record 行。
驗證錯誤
空 data 的 byte-exact parse detail:
[{"type": "too_short", "loc": ["record", "data"], "msg": "Dictionary should have at least 1 item after validation, not 0", "url": "https://errors.pydantic.dev/<pydantic-version>/v/too_short"}]本機 ref/key validation:
table 'orders' has no resolvable key column for record matching
key column 'active' is a boolean column and cannot be a natural key (a two-value domain cannot uniquely match records)
key column 'payload' is a json column and cannot be a natural key (json values have no canonical bind representation and cannot uniquely match records)
key column 'span' is a interval column and cannot be a natural key (interval values have no canonical bind representation and cannot uniquely match records)
unknown column ref 'missing' in record.datainterval 那一則照原文寫作 is a interval column。這三種型別都無法以值比對 live row,用它們當 key 會讓每次 apply 都重新插入所有種子列;plan 的位置預設因此會跳過它們,而這些訊息會拒絕明確宣告或已記錄的這類 key。只有帶 record line 的表才會檢查。
身分 token 的失敗以 cell 為單位回報——完整目錄請見可攜身分 token:
principal token '$user:bob': no user with username 'bob' in this company
principal token '$smc:line:U123': ambiguous — 2 social clients match, in chatrooms ['<id-a>', '<id-b>']
principal token '$room:Support': ambiguous — 2 live chatrooms are named 'Support' (['<id-a>', '<id-b>']); qualify it as '$room:<department name>/Support'
principal token '$smc:line:U123' does not match the column's type: a user column expects $user:<...> or a raw id
principal token '$dept:Sales' does not match the column's type: a principal column expects $user:<...> or $smc:<...> or $room:<...> or a raw id若某一行 record 用到的 principal 欄位是在同一份文件較早處宣告、尚未建立的欄位,plan 階段無法分類它。該行會 plan 成 insert、沒有錯誤,只帶一則 warning——record classification deferred to apply (references a same-doc column not yet created)——而且 applyable 維持 true。Executor 會自己重跑一次替換,所以那個欄位上壞掉的 token 改在 apply 階段浮現:回應是 200,出錯的那一行回報 applied: false 與 principal column '<internal key>': <同一則訊息>(用的是內部 key,不是顯示名稱),文件層級的 errors[] 帶同一則 detail 並標記 phase: "apply",而同一個新欄位上的其他行仍然會套用。
Server plan 看見 live rows 後還可能回傳:
record data missing required key column value ('col_a1b2c3')
duplicate key value 'A-100': 2 record lines in this document declare the same new key
ambiguous key value 'A-100': 2 live records match
key column not resolvable: <detail>這些是 diff 階段錯誤:apply 回 200,只略過受影響的那一行。欄位容納不了的 decimal cell 是例外——decimal column '<internal key>': … 是 validate 階段錯誤,會擋下整份文件(見 decimal cell)。
精確本機 detail 來源:components/iac/parse.ts。Live duplicate/ambiguity 與身分 token 解析只能由 server plan 判定。
動手試試
在 IaC 工作台宣告 orders、order_no 與範例 record,先刪除 table key 與所有可用 columns 觀察 key error,再恢復。對測試 scope plan/apply 後重新 plan,確認 record action 收斂為 skip。