# Pattern 06 — UPDATE without WITH CHECK (row ownership transfer)

## The failure

AI generates an UPDATE policy with USING only:

```sql
CREATE POLICY "update_own_post" ON posts
  FOR UPDATE USING (auth.uid() = user_id);
```

USING controls which rows a user can run UPDATE on — they can only target their own rows. But without WITH CHECK, there's no constraint on what the UPDATE can change those rows *to*. A user can do:

```sql
UPDATE posts SET user_id = '<some_other_users_uuid>' WHERE id = <their_post_id>;
```

This succeeds. The post now belongs to another user. Depending on your SELECT policy, that other user may now see this post in their feed. The original user has lost their post. A malicious user can also use this to leave a trail in another account.

## The fix

Add WITH CHECK to assert that the new state of the row also satisfies the ownership constraint:

```sql
CREATE POLICY "update_own_post"
  ON posts
  FOR UPDATE
  TO authenticated
  USING (auth.uid() = user_id)        -- can only UPDATE rows you currently own
  WITH CHECK (auth.uid() = user_id);  -- result row must still have your user_id
```

With both clauses, a user who tries to change `user_id` gets: `ERROR: new row violates row-level security policy for table "posts"`.

## How USING + WITH CHECK work together for UPDATE

PostgreSQL evaluates UPDATE policies in two steps:

1. **USING** — applied to the *current* row (before the update). If USING returns false, the UPDATE can't touch this row (it's filtered out, as if it doesn't exist).
2. **WITH CHECK** — applied to the *proposed new row* (after the update is applied). If WITH CHECK returns false, the UPDATE is rejected with an error.

For most ownership policies, the condition is identical for both: `auth.uid() = user_id`. This prevents:
- Updating rows you don't own (USING)
- Changing `user_id` to someone you're not (WITH CHECK)

## Testing it

```sql
-- As user A: insert a post you own
INSERT INTO posts (user_id, title) VALUES (auth.uid(), 'My post');

-- As user A: try to transfer it to user B
UPDATE posts SET user_id = '<user_B_uuid>' WHERE title = 'My post';
-- Should fail: new row violates row-level security policy

-- Confirm the post still belongs to you
SELECT user_id FROM posts WHERE title = 'My post';
-- Should still be your UUID
```
