updateは0行でもエラーにならないのに、insertはエラーになる

転職用のポートフォリオとして、個人でWebアプリ「Hubpin」を作っています。
制作の経緯は「Hubpin 開発記録」に時系列でまとめています。
設計と判断は自分で行い、実装も原則自分で書いています。AIには手順書・レビュー・調査を任せています。

RLS(行単位のアクセス制御)シリーズの5本目です。
前回は「他人のデータが見えないこと」を確認する側の話でした。
今回は書き込み側で、他人のデータを更新しようとしたら「成功」が返ってきた話です。

目次

更新は通るのに、追加は弾かれる

他人のカードを更新しようとしたときの挙動がこうでした。

  • 更新(update) … エラーは null。0行が更新されただけ。呼び出し側からは成功に見える
  • 追加(insert) … エラーになる(42501

同じテーブル、同じ RLS なのに、片方は静かに 0 行で、片方はエラーです。
最初はアプリ側のコードを疑いましたが、原因はそこにありませんでした。

答えはポリシーの「句」の違いだった

見に行ったのはマイグレーションファイルでした。ポリシーの書き方が違いました。

create policy "users can insert own items"
  on public.items for insert with check (auth.uid() = user_id);

create policy "users can update own items"
  on public.items for update using (auth.uid() = user_id);
ポリシー 他人の行を触ると
更新 using 対象から外れる → 0行・エラーは null
追加 with check 値を検査する → エラー

using は「どの行が対象か」を絞る条件です。
絞った結果 0 行になっても、条件に合う行が無かっただけなので、違反ではありません。
一方 with check は「入れようとしている値」を検査するので、通らなければエラーになります。

PostgreSQL の公式ドキュメントにも、そのとおり書いてありました。

Existing table rows are checked against the expression specified in USING,
while new rows that would be created via INSERT or UPDATE are checked against
the expression specified in WITH CHECK.

using で除外された行については、こうあります。

Typically, no error occurs when a row is not visible

エラーが起きないのが通常の動作でした。
自分のコードが握りつぶしていたわけではありませんでした。

0行を自分で見るしかない

エラーが出ないので、0行だったことを自分で確かめるしかありません。
更新の呼び出しをこう変えました。

const { data, error } = await supabase
  .from('items')
  .update({ ... })
  .eq('id', id)
  .select('id')
  .maybeSingle()

if (error) { /* 障害 */ }
if (!data) { /* 0行だった */ }

書いてみると短いのですが、注意点が3つありました。

1. error を見た「あと」に data を見る。
逆順にすると、error があるときは datanull なので、
本物の障害が「見つかりません」に化けます。

2. .select() を足すと、select のポリシーも通る必要がある。
自分のテーブルには閲覧のポリシーがあるので通りましたが、
更新のポリシーだけあって閲覧のポリシーが無いテーブルだと、
「更新は成功したのに 0 行が返る」という逆パターンになります。

3. 0行の理由は2つあって区別できない。
他人のものだったのか、そもそも存在しなかったのか。
なので画面の文言は「データが見つかりません」にしました。確認していないことは書かない、という理由です。

削除でも同じだった

その後、削除の機能を作るときに同じ問題が出ました。
削除のポリシーも using で書いてあるので、他人の行を消そうとしても静かに 0 行です。
更新と同じ形(.select('id').maybeSingle()data を見る)で書きました。

公式ドキュメントの表を見ると、using を使うのは SELECT・UPDATE・DELETE、
with check を使うのは INSERT・UPDATE でした。
追加だけがエラーになったのは、追加だけが with check 側だったからです。

ついでに知ったこととして、UPDATE のポリシーは両方の句を持てます。
with check を書かない場合は、using の条件が両方に使われます。
自分の書き方は後者でした。

行を絞ることと、値を弾くことは別だった

このシリーズで4回書いてきたのは、
「RLS は拒否の仕組みというより、見える範囲を決める仕組みだ」ということでした。
今回は、それが書き込み側でも同じ形で出たという話です。

同じ RLS でも、どちらの句で書いたかで「エラーが出るか、静かに 0 行になるか」が変わる。
静かに 0 行になるほうは、自分で見にいかないと気づけません。
見に行く順番まで含めて、書き方が決まっていました。

目次