Row policy tokens
Row policy 的 predicate value 除了字面值,也可以放 token。Token 以原字串儲存在 grant 上,等到伺服器解析該 principal 的權限時才代換成當次請求的literal,因此同一筆 grant 就能對所有檢視者同時表達「只有我的列」或「未來七天」。
Token 是 ACL row policy 專屬的機制。它只在 read_filter / edit_filter 內解析;模組中其他 filter lane 不是在 authoring 時直接拒絕,就是把它當成永遠不會命中的死字串。
Token 詞彙
| Token | 解析成 | 代換後的literal |
|---|---|---|
$me | 當下 principal 自己的 id——在 principal 欄位上則改解析成一個SET(tagged cell 的集合,見 principal 欄位上的 SET 語意) | 原始 id 字串,或該 SET |
$me.department | 當下 user 的 department_id,是 department UUID,不是名稱;在 principal 欄位上則改解析成該部門的 tagged cell SET | 原始 id 字串,或該 SET |
$today | 以 Asia/Taipei 日界線計算的今天 | date cell 為 2026-07-25;datetime cell 為 2026-07-25 00:00 |
$today+Nd / $today-Nd | 今天之後/之前 N 天,N 為 1–730 的整數 | 與 $today 相同語法 |
$now | 台北當下牆上時間,向下取整到分鐘 | 2026-07-25 14:03(僅 datetime cell) |
比對是精確且區分大小寫,也不做 trim。$Me、$me (尾端空白)、$mee、$today+0d、$today+731d、$today+2h、$Now、$now+2h、$nowx 都不是 token,會落入下方的 $ 保留規則並在 authoring 時被拒絕。沒有 $now±Nh,也沒有 $today±Nh。
$me 同一個寫法背後其實有三種 id 空間:
- 內部 user 解析為自己的 user id;
- 外部 social client 解析為 client id,所以
$me也能合法命中social_client欄位或存放 client id 的 string 欄位; - agent 平台的 client 解析為其連結 user 的 id,因為該平台會委派給內部 user 的 resolver。
$today 與 $now 使用固定的 +08:00 Asia/Taipei offset,不是可設定的 IANA 時區。Row policy 沒有 timezone 欄位,而且 grammar(and / or / not 包住 predicate 與 link_target leaf)不會為此擴充,因此跨區租戶拿到的是台北的日界線。Scheduled trigger 是另一條 lane,它有自己的 timezone 欄位。代換後的日期時間分隔符是空白,不是 ISO 的 T——因為 cell 比較是字典序字串比較。
原樣儲存,只在三個出口解析
Token 以原字串儲存在 grant 上,只在三個呼叫點被代換:內部 user 的 permission resolver、social client 的 permission resolver,以及 public-read 的 resolver,除此之外都不會。代換發生在 can_edit → can_read 拉齊之後,回傳新物件,永遠不會就地改寫已儲存的資料列。
前端必須據此設計的兩件事:
- Grant 的 read / list 端點回傳的是原始 token,不是解析後的literal。 權限編輯 UI 必須把 token 字串原樣往返;把畫面上解析出的日期存回去,等於把 grant 去 token 化,
$today會被凍結在有人打開編輯器的那一天。IaC export 也是逐字輸出 token,這正是套用過的文件能 re-plan 出 0-diff 的原因。 - 解析刻意不做快取。 同一張表對不同檢視者、甚至同一位檢視者在下一分鐘,都會合法地回傳不同的列與不同的總數。任何被快取的筆數、匯出的 CSV、分享的截圖,都同時綁定檢視者與時間。
$ 前綴是保留字
在 row policy 的 predicate value 位置,整個 $ 前綴都是保留的。任何不屬於這五個 token 的 $ 開頭字串,會在 authoring 時以 422 拒絕:
row policy and[0] value '$mee' is not a valid policy token — '$'-prefixed values are
reserved for tokens in row policies; valid tokens: $me, $me.department, $today,
$today+Nd, $today-Nd (N 1..730), $now這在 authoring 階段就消滅了打錯字這一類問題:拼錯的 token 不會變成安靜的死字串。相對地,這也代表以 $ 開頭的合法業務值(價格字串、樣板片段)永遠不能當成 row policy 的操作元。
保留規則是依位置生效的,只作用在 ACL row policy 的 value 位置。模組中其他 $ 語法在不同位置,完全不受影響:IaC identity tokens($user:、$dept:、$smc:、$room:)、command 的 $ctx. / $input. / $row.、link 比對的 $self、trigger 樣板的 $row、rule 參照的 $row. / $same。反過來說,把 IaC identity token 貼進 predicate value 會被這條保留規則擋下,把 row policy token 貼進 IaC 的 principal.id 則會被當成無法辨識的 principal token。
Token、欄位型別與 operator 必須相符
配對在 grant 寫入時檢查,違反時回 422。
| Token | 欄位型別 | Operators | 可否放進 in list |
|---|---|---|---|
$me | user、social_client、string、text、principal(SET 語意,見下方) | eq、neq | 可以,且能與literal id 混用 |
$me.department | string、text、principal(SET 語意,見下方) | eq、neq | 可以 |
$today[±Nd](date 欄位) | date | eq、neq、gt、gte、lt、lte | 不可 |
$today[±Nd](datetime 欄位) | datetime | 只有 gt、gte、lt、lte | 不可 |
$now | 只有 datetime | gt、gte、lt、lte | 不可 |
select 刻意被排除在 $me 之外:select 的選項是作者定義的封閉詞彙,用它來裝 user id 是反模式。在 string / text 欄位上,$me.department 必須指向存放 department id 的欄位;存團隊或分店名稱的欄位不會命中任何列。在 principal 欄位上則不需要這個慣例——該欄位本來就存放 tagged 的 user: / smc: / room: cell。
有兩種拒絕是 principal 欄位型別專屬的,在對它撰寫 policy 前值得先知道:
- 日期類 token 在那裡會被拒絕。
$today[±Nd]與$now需要date或datetime欄位,所以放在principal欄位上,grant 寫入會以row policy and[0] token '$today' requires a date or datetime column — column 'col_…' is type principal失敗。一個日期literal永遠不可能等於 tagged cell;就算有人從未驗證的建表 settings lane 塞進來,resolver 也會讓該條 leaf fail closed。 - literal操作元本身必須是 tagged cell。
principal欄位上的eq/neq/in只收user:<id>、smc:<id>、room:<id>——裸 id、actor key、IaC 的$user:token 都會被拒:row policy and[0] column 'col_…' principal filter operand must be a tagged principal cell (user:<id>, smc:<id> or room:<id>)。舊的user/social_client欄位仍然收原本那種裸 id 字串。cell 文法本身見principal欄位型別。
principal 欄位上的 SET 語意
principal 欄位的一個 cell 就是一個 tagged 的負責人:user:<id>、smc:<id> 或 room:<id>,永遠不是裸 id。在這種欄位型別上,$me 與 $me.department 完全不會解析成一個 scalar id:兩者都解析成一個 tagged cell 的 SET,eq / in 直接對整個 SET 編譯。SET 裡有什麼,取決於是誰在問——如果是在房間內的呼叫者,還取決於在哪個房間問:
| Lane | $me SET | $me.department SET |
|---|---|---|
| REST(沒有 acting room) | 呼叫者的 user:<id>,加上呼叫者所屬每個存活、同公司房間各一個 room:<id> | 呼叫者部門內每個存活、同公司 user 的 user:<id>(含呼叫者本人),加上該部門每個存活、同公司房間的 room:<id> |
| Agent toolkit/compose commands(acting room 已釘住) | 呼叫者的 user:<id>,加上 acting room——但僅限呼叫者是該房間的存活成員,否則只有自己 | users 那一腿不變;room 那一腿只在 acting room 屬於該部門時才保留,否則沒有 room 腿 |
ScpDeny 載體(trigger、public callback) | 只有自己——即使旁邊傳了明確的 acting room 也一樣蓋過去 | 只有 users 那一腿、沒有 room 腿——同樣的蓋過規則 |
| Social client(LINE/FB/IG) | 只有呼叫者的 smc:<id>,永遠沒有房間——一個 OA channel room 是該帳號所有客戶共用的 | 永遠不會——social client 沒有部門,這條 lane 永遠 never-match(根本沒有 department getter) |
| Public read token | 永遠不會——不存在任何 principal | 永遠不會——不存在任何 principal |
Agent 平台的 client 會委派給內部 user 的 resolver,繼承上面的 user lane,acting room 也透過同一個釘住機制傳遞。
room 腿由兩條規則決定,而且因為 $me 與 $me.department 共用同一個釘住 helper,兩者完全一致:
$me的 room 腿看成員身分,$me.department的看部門。$me收集呼叫者所屬的存活、同公司房間。$me.department收集department_id等於呼叫者部門的存活、同公司房間——成員身分不是判斷條件:部門房間不管呼叫者有沒有在裡面都算,部門同事不管有沒有跟呼叫者共處一室也都算。$me.department的 users 腿同樣是該部門所有存活、同公司的 user,不論他們待在哪個房間或不在任何房間。- 釘住只會減少,而且優先序固定。 依序是:
ScpDeny載體(trigger 執行、public callback 寫入)最優先,直接完全沒有 room 腿,即使旁邊同時傳了 acting room 也一樣;其次是明確傳入的 acting room;再其次是已 stash 的ScpBinding,釘到它的房間;都沒有時該 lane 不釘住,套用 REST 語意。明確的 acting room 與已 stash 的 binding 指向不同房間屬於程式錯誤,伺服器的回答是沒有 room 腿加上一則 error log,而不是挑一個。被釘住的房間仍必須通過未釘住查詢的每一項檢查——存活、公司錨點,再加上成員身分($me)或部門檢查($me.department)——所以釘住永遠不會加進未釘住 SET 裡本來沒有的房間。
兩個 token 可以放在同一個 in list,此時該條 leaf 會編譯成單一個對兩者聯集的 in——「指派給我、或指派給我部門任何人的列」:
{
"column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa",
"op": "in",
"value": ["$me", "$me.department"]
}每個 token 的 SET 在一次解析中最多只計算一次、且各自記憶,read_filter 與 edit_filter 共用同一份快照,因此兩者不會對「誰在部門裡」有不同答案。
改寫規則(沒有新 operator——還是既有的 eq / neq / in 文法,只是比對對象從 scalar 換成 SET):
| 原始 leaf | 解析成 |
|---|---|
{column, eq, "$me"}(或 "$me.department") | {column, in, SET} |
{column, in, [literal…, "$me", "$me.department", …]} | {column, in, literals ∪ 每個被引用的 SET}——合成一個 in list;每個literal都必須能解析成 tagged cell(user: / smc: / room:),否則整條 leaf 失敗 |
{column, neq, "$me"} | {"and": [{column, is_not_null}, {"not": {column, in, SET}}]}——存在且不在 SET 內;排除空值,與 neq 在其他欄位上的 presence 規則一致 |
{"not": {column, eq, "$me"}} | {"not": {column, in, SET}}——空值或不在 SET 內;包含空值,與其他地方 not(eq) 的規則一致 |
SET 一律 fail-closed 且永不截斷:getter 回傳 None 或 []、SET 內有 id 落在 cell 文法之外、in list 裡有literal無法解析、欄位上出現 $today / $now token、eq / neq 收到 list 操作元、或 SET 超過 ROW_POLICY_PRINCIPAL_SET_MAX(1000 筆),都會讓那條 leaf 失敗——套用與其他無法解析 token 相同的 and/or/not polarity(見下方解析失敗一律 fail closed,且有 polarity)——而不是整筆 grant 失敗,也不會安靜地縮成一個殘缺的子集合。當一個 in list 混合了literal與 $me / $me.department 時,只要有一個 token 失敗或一個literal無法解析,就會讓整條 leaf 失敗,而不只是失敗的那個 arm:只丟掉失敗的 arm、保留其餘 arm,在 not 之下是 fail-open 的(存活 arm 的補集,會比原本聯集的補集更寬)。
1000 筆是解析階段的上限,與 authoring 階段每個 in list 最多 100 個元素是兩回事:你最多寫 100 個操作元,其中每個 token 展開後的 SET 與其餘元素加總必須低於 1000。
舊有的 user / social_client / string / text 欄位不受影響:$me 在這四種型別上仍解析成原本的 scalar id,$me.department 在 string / text 上仍解析成 scalar department_id(它本來就不接受 user / social_client)。
要查一個可以寫入的 tagged cell,使用 custom_tables_resolve_principal(kind:user | social_client | chatroom——chatroom 只列出呼叫者所屬的存活房間,不是全公司房間目錄),並把回傳的 ref 原樣寫回;不要自己組 tag:id 字串。這個工具只在有已驗證的內部 principal 時才會建立,所以外部 client lane 根本沒有目錄可用。
有兩種拒絕會直接告訴你正確寫法:
datetime欄位上的eq "$today"因為 cell 帶分鐘精度而不可能成立,會被拒絕。請改寫成半開區間:同一個andlist 內放gte "$today"與lt "$today+1d"兩個 predicate。date欄位上的$now會以token '$now' is intraday; a date column cannot express it — use $today instead拒絕。
實例
只看自己的列。 own 固定看 created_by;$me 把它推廣到任何負責人欄位。
{
"can_read": "filtered",
"read_filter": {
"and": [
{ "column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa", "op": "eq", "value": "$me" }
]
}
}自己的列加上共用信箱。 in list 的每個元素獨立解析,所以 token 與literal id 可以放在同一個 list。
{
"column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa",
"op": "in",
"value": ["$me", "33333333-3333-4333-8333-333333333333"]
}自己部門的列。 在 string / text 欄位上,cell 必須存 department id;在 principal 欄位上,同一條 predicate 的意思是「指派給我部門任何人、或該部門任何房間」,完全不需要任何 id 慣例。
{ "column": "col_bbbbbbbb_bbbb_4bbb_8bbb_bbbbbbbbbbbb", "op": "eq", "value": "$me.department" }不是我的列。 neq 保有它的 presence gate,因此沒有負責人的列不會進來;若你要把它們納入,用 or 明講:
{
"or": [
{ "column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa", "op": "neq", "value": "$me" },
{ "column": "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa", "op": "is_null" }
]
}透過 link 找自己的列。 link_target leaf 會沿著一個 link 欄位走進被連結的表,而 target 內的 token 是對被連結欄位的型別配對——所以下面這條的意思是「負責業務那筆 Staff 列的 TS 帳號欄位是我的案件」:
{
"link": "col_eeeeeeee_eeee_4eee_8eee_eeeeeeeeeeee",
"quantifier": "any",
"target": { "column": "col_ffffffff_ffff_4fff_8fff_ffffffffffff", "op": "eq", "value": "$me" }
}未來七天(date 欄位):
{
"and": [
{ "column": "col_cccccccc_cccc_4ccc_8ccc_cccccccccccc", "op": "gte", "value": "$today" },
{ "column": "col_cccccccc_cccc_4ccc_8ccc_cccccccccccc", "op": "lt", "value": "$today+7d" }
]
}今天(datetime 欄位)——就是上面那個 eq 拒絕訊息要求的半開區間:
{
"and": [
{ "column": "col_dddddddd_dddd_4ddd_8ddd_dddddddddddd", "op": "gte", "value": "$today" },
{ "column": "col_dddddddd_dddd_4ddd_8ddd_dddddddddddd", "op": "lt", "value": "$today+1d" }
]
}哪些介面接受 token
| 介面 | 行為 |
|---|---|
| User、department、chatroom、client grants(含 bulk lane) | 每次請求解析 |
default_permissions | 每次請求解析 |
Client access 的 passphrase_permissions | 解析;原始 token 會被複製到每個加入 client 的 grant,之後每次請求各自解析 |
IaC 的 grant 與 client_access line | 解析;只有 predicate 的 column 會被翻譯,token value 原樣通過 |
| Public-read token | 只接受日期類 token:$today[±Nd] 與 $now 可用,$me 與 $me.department 在 mint 時被拒 |
Rollup 欄位 filter、rule 的 when / check / compare、SCP policy scalar leaf、ad-hoc aggregate filters | 拒絕,400 |
Trigger when predicate 與 materialize_slots 的 source.filter | 拒絕,400 |
Query 的 stored_filters / computed_filters | 完全不解析,當成一般literal |
Config 與 aggregate lane 共用同一則拒絕訊息:
filter value '$me' on column 'Owner' — identity/date tokens ($me, $today…) are only
valid in ACL row policies. use a literal value here; tokens resolve only inside grant
read_filter/edit_filter row policies那些 lane 與檢視者無關——儲存的 computed cell、背景執行的 rule 評估、channel floor——$me 在那裡沒有定義好的 principal,因此在 authoring 時拒絕是唯一安全的語意。Public-read lane 只拒絕身分那一半:read_filter may not reference $me / $me.department on a public token (no principal exists); $today is allowed(422;IaC public_read line 回報的同一錯誤字尾是 …; $today and $now are allowed);這道檢查會走遍整棵 and / or / not 樹,包含 link_target 的 target 內部,所以身分 token 沒辦法藏在下一層偷渡進去。
link_target 內的 token
link_target leaf 沿著一個 link 欄位走進被連結的表,它的 target 就是一棵一般的 row-policy 子樹。Token 在那裡合法,行為也一樣,只有一條規則要記得:target 內的 token 是對「被連結那張表」的欄位型別配對與解析,不是本表的。 被連結表的 principal 欄位上,$me 一樣解析成 principal SET;被連結表的舊 user 欄位上,$me 解析成 scalar id;$today 需要的是那一邊的 date / datetime 欄位。Grant 寫入時會拿被連結表的 schema 驗證 target 的 leaf,所以配錯的 token 是 authoring 當下的 422,不是安靜的永不命中。
如果 link 本身在讀取時無法解析——link 欄位被刪、被連結的表被軟刪除、quantifier 壞掉——整條 leaf 會在編譯任何 SQL 之前就按下方的 polarity 規則 fail closed。
Query lane 的陷阱。 Search 與 query 端點的 stored_filters / computed_filters 完全沒有 token 分支。$me 在那裡就是一般字串:不會被代換,因此安靜地不命中任何列(若比對到數值或日期欄位則回 4xx)。你無法靠把 token 傳進 search 來做出「只看我的」快捷篩選——請改用 grant row policy,伺服器本來就會套用在每次讀取上。
解析失敗一律 fail closed,且有 polarity
無法解析的 token 是在它所在的那條 leaf 上 fail closed,不是整條 policy——而「fail closed」實際的意思,取決於那條 leaf 落在 and / or / not 樹的哪個位置(與 grant 類型與 row policy 對其他無法解析節點所講的是同一條 polarity 規則——token 解析失敗只是其中一個實例,這兩頁的說法必須一致):
- 落在
and內(含把扁平 predicate list 包起來的隱含and)——整組不命中任何列; - 落在
or內——那個 arm 被丟掉,其他 arm 照常套用; - 落在
not內——那個 arm 變成永不命中,not(永不命中)不會反轉成全部命中,因為失敗本身就已經是安全、較窄的答案; - 若失敗的那條 leaf 本身就是 root——沒有布林包裝的裸 predicate,或整棵樹被解析到全部消失——有效 filter 就是
{"and": []},也就是所有 enforcement lane 都會拒絕的永不命中 policy。這種「整棵樹塌陷到 root」的情況是例外,不是常態;多數 policy 在失敗的 leaf 旁邊還有其他 sibling 繼續生效。
代換永不截斷:一個 token 要嘛完整解析——解析成一個literal,或在 principal 欄位上解析成整個 SET——要嘛直接失敗;沒有部分值,也沒有只解析一半的 SET。
以下情況算無法解析:
$me沒有 principal id,或 id 為空;$me.department用在沒有部門的 user,或用在任何 social client——client lane 根本不傳部門 getter,所以帶$me.department的 client 面向 grant 在結構上就是永不命中;- 在
principal欄位上,$me/$me.department的 getter 回傳None或空 SET、SET 內有 id 落在 cell 文法之外、inlist 裡有literal無法解析成 tagged cell,或 SET 超過ROW_POLICY_PRINCIPAL_SET_MAX(1000 筆)——見 principal 欄位上的 SET 語意; - 日期或
$nowtoken 落在型別不符或型別未知的欄位——包含principal欄位,因為日期literal永遠不可能等於 tagged cell; - 日期或
$nowtoken 出現在inlist 內; - 這類 token 需要的 table 讀不到;
link_target的 link 欄位、被連結的表或 quantifier 無法解析——該條 leaf 在編譯前就失敗,因此外層的not永遠不可能把它反轉成全部命中。
伺服器永遠不會猜一個literal代進去,缺席的身分也永遠不會被當成萬用字元。型別合法但配錯對象的 token——例如 user 場景的 $me 放在 social_client 欄位——是合法但不會命中的設定;那裡看到零列是預期結果,不是 bug。
若未解析的 token 真的抵達 enforcement lane,SQL builder 與其 Python twin 都會輸出恆假條件,而不是把 token 當字串編譯。少了這道防線,任何能新增資料的使用者只要把 $me 這個字串寫進自己的 cell,就會出現在每個 filtered 檢視者的範圍裡。
空值 cell 不會通過任何值比較
Row policy 會把空值 cell 排除在所有值比較之外。eq、neq、gt、gte、lt、lte、in、contains 在空值 cell 上一律為 false,SQL 讀取 lane 與 Python 寫入檢查 lane 都一樣。is_null 與 is_not_null 是唯一能詢問「是否為空」的方式。
「空」只有三種情況:
- JSON key 不存在,extract 為 SQL
NULL; - cell 存的是明確的 JSON
null; - cell 存的是空字串
""。
falsy 但真實的值仍然算存在、仍然會命中:0、false、"0" 都已釘住。空陣列則根本不可能出現,因為 row policy 只接受 scalar 欄位型別,而兩種可存 list 的型別(json、multi_select)都被排除。
實務上最常見的空值形式是明確的 JSON null,因為建立 record 時會為每個未填欄位寫入 null。在 MySQL 上,對明確 JSON null 直接 unquote 會得到非空字串 'null',這就是 presence gate 由三段條件組成、並以 JSON type 當判別依據的原因。
這項變更會縮小既有租戶的可見範圍。 Row policy 的 SQL lane 已修正為與 Python twin 一致,值比較不再命中空值 cell。任何形如 neq $me、neq <literal>、範圍比較或 contains、且作用在有空白 cell 欄位上的既有 policy,現在會回傳更少的列。具體來說:範圍是 filed_by neq $me(「不是我送出的」)的審核者,將不再看到送出者為空的發票。上線前請先盤點租戶實際在跑的 row policy。
Grammar 長出 or / not 之後,policy 可以在同一條 filter 內寫成「neq $me 或 is_null」——這就是把空白列找回來的正規寫法(見上方不是我的列範例)。另一種寫法是 {"not": {col eq "$me"}}:它是嚴格補集,結構上就包含空白列。當空白是資料問題而不是 policy 問題時,回填欄位仍是更乾淨的做法。
自 teamsync-backend PR #1169(值運算子)與後續的 #1170(is_null)起,一般 query lane 也遵守同一條規則,所以現在用同一套 null 規則就能描述每一條 lane:
neq在每一處都排除空白。 Grantread_filter、stored_filters、agent 工具組 filter、command query 裡的neq "x",都會排除 key 不存在、明確 JSONnull與儲存的空字串。修正之前,query lane 的neq在string/text/boolean欄位上會命中儲存的""與明確 JSONnull,row policy 則會排除它們。is_null在每一條 lane 都是同一個 predicate。 Key 不存在、明確 JSONnull、儲存的""都滿足is_null,而is_not_null正好是它的補集——row policy 與 query filter 皆然。修正之前,query filter 的is_null只檢查 JSON 的 null 狀態,所以儲存的""在 row policy 裡滿足、在 query 裡卻不滿足。json欄位是唯一刻意保留的例外,而它本來就不能出現在 row policy 裡。JSON 的""在那裡是真實的文件,所以 query filter 在json欄位上的is_null只代表 key 不存在或 JSONnull。Query lane 的完整說明見查詢。
Authoring 陷阱
contains只用在string與text欄位。 Row policy 在寫入時不會對contains與排序類 operator 做型別檢查,所以contains放在 integer、float、date、datetime 欄位會被 grant 端點接受,然後在編譯讀取查詢時失敗——表現為該 principal 每次讀這張表都拿到500,而不是 grant 當下的4xx。contains有兩份實作。 Python lane 用區分大小寫的子字串比對;SQL lane 用LIKE,大小寫敏感度跟著欄位 collation。大小寫不同的值因此可能在讀取與寫入 post-image 檢查上表現不一致。優先使用eq或in。$now向下取整到分鐘且不做逐 operator 的進位/捨去,gt、gte、lt、lte看到的是同一個literal,跨分鐘邊界的影響上限是 60 秒。- 系統
id條件在 2026-07-28 活了過來。 policy 條件裡提到系統id欄位時,SQL lane 與 Python twin 現在都對準真正的主鍵。在那之前它被編譯到幽靈 uuid 上,policy 提到id的filteredgrant 其實是無聲的永不命中。這些存量 policy 現在會給出它們一直宣稱要給的可見範圍——在假設某個 principal 的範圍不變之前,先盤點它們。文法載不動 row identity 的 SCP scalar leaf 則在撰寫時直接拒絕id,不再存下半殘的 policy。 - 透過建表時的自由格式
settings塞進去的 policy 不會做 grammar 檢查。 只有專屬的 grant、default-permissions、client-access、public-read 端點會驗證。這種 policy 會 fail closed 成零列而不是報錯,是最難除錯的 authoring 路徑。
Token 所在的 grant 形狀見 Grant 類型與 row policy,哪一筆 grant 提供 policy 見 effective permissions,可攜的文件形式見 IaC grant line。