CREATE_POLICY(7)

๐Ÿ’… CREATE POLICY(7) โ€” PostgreSQL 18.6 Documentation โœจ

okay bestie, sit down, hydrate, because today we're learning about CREATE POLICY and honestly? it's giving gatekeeper energy but we're gonna unlock it together ๐Ÿซถ


๐ŸŒธ NAME

CREATE_POLICY โ€” define a new row-level security policy for a table ๐Ÿ’–


โœจ SYNOPSIS

CREATE POLICY name ON table_name
    [ AS { PERMISSIVE | RESTRICTIVE } ]
    [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
    [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ]
    [ USING ( using_expression ) ]
    [ WITH CHECK ( check_expression ) ]

look at her. so structured. so much power in so few brackets ๐ŸŽ€


๐Ÿ’ซ DESCRIPTION

So the tea is: CREATE POLICY makes a brand-new row-level security policy for a table. But โ€” and this is the part everyone skips and then cries about โ€” row-level security has to actually be turned ON for the table (that's your ALTER TABLE ... ENABLE ROW LEVEL SECURITY) or all these cute little policies you're writing? they do literally nothing. dead. ๐Ÿ’€

A policy is basically the bouncer that grants permission to select, insert, update, or delete rows that match the relevant policy expression. Here's the split:

  • rows that already exist get checked against the expression in USING
  • rows that would be created via INSERT or UPDATE get checked against the expression in WITH CHECK

When a USING expression returns true for a row, that row is visible to the user ๐Ÿ‘€. When it returns false or null, the row is just... not visible. And typically? no error. it just quietly disappears. (but see Table 300 for the exceptions, because of course there are exceptions ๐Ÿ™ˆ)

When a WITH CHECK expression returns true for a row, that row gets inserted or updated. When it returns false or null? error. thrown. no silent treatment here, she's loud about it ๐Ÿ˜ญ

For INSERT, UPDATE, and MERGE statements, WITH CHECK expressions are enforced after BEFORE triggers are fired, and before any actual data modifications are made. so a BEFORE ROW trigger can modify the data to be inserted, which then affects the result of the security policy check. (yes fr fr, that's real.) and WITH CHECK expressions are enforced before any other constraints. she's first in line, always ๐Ÿ’…

Policy names are per-table. so you can absolutely use the same policy name on a bunch of different tables, and each table gets its own definition that's appropriate for it. slay, honestly.

Policies can be applied for specific commands or for specific roles. default for newly created policies? they apply to all commands and all roles, unless you say otherwise. multiple policies can apply to a single command โ€” more on that below. and Table 300 summarizes how the different types of policy apply to specific commands ๐Ÿ“‹

Now for the โœจplot twistโœจ โ€” for policies that can have both USING and WITH CHECK expressions (that's ALL and UPDATE), if you don't define a WITH CHECK expression, then the USING expression does double duty: it determines which rows are visible (the normal USING case) and which new rows are allowed to be added (the WITH CHECK case). one expression, two jobs, main character energy ๐Ÿ‘‘

And if row-level security is enabled for a table but there are no applicable policies at all? a "default deny" policy is assumed, so no rows will be visible or updatable. nothing. zero. the void. ๐Ÿ•ณ๏ธ


๐ŸŽ€ PARAMETERS

  • name โ€” the name of the policy you're creating. it has to be distinct from the name of any other policy for that table. no twins allowed ๐Ÿ’–
  • table_name โ€” the name (optionally schema-qualified) of the table the policy applies to.
  • PERMISSIVE โ€” says this policy is created as a permissive policy. all permissive policies applicable to a given query get combined together using the Boolean "OR" operator. so by creating permissive policies, administrators can add to the set of records that can be accessed. and psst โ€” policies are permissive by default ๐ŸŒธ
  • RESTRICTIVE โ€” says this policy is created as a restrictive policy. all restrictive policies applicable to a given query get combined together using the Boolean "AND" operator. so by creating restrictive policies, administrators can reduce the set of records that can be accessed, because all restrictive policies must be passed for each record.
heads up: there needs to be at least one permissive policy granting access to records before restrictive policies can be usefully used to reduce that access. if only restrictive policies exist, then no records will be accessible. when you've got a mix of permissive and restrictive policies, a record is only accessible if at least one of the permissive policies passes, in addition to all the restrictive policies. both. always both. ๐Ÿ’…
  • command โ€” the command the policy applies to. valid options are ALL, SELECT, INSERT, UPDATE, and DELETE. ALL is the default. see below for the specifics on how these get applied.
  • role_name โ€” the role(s) the policy is applied to. default is PUBLIC, which applies the policy to all roles. everyone's invited ๐Ÿซถ
  • using_expression โ€” any SQL conditional expression (returning boolean). and no, the conditional expression cannot contain any aggregate or window functions, so don't even try it ๐Ÿ™ˆ. this expression gets added to queries that refer to the table if row-level security is enabled. rows for which the expression returns true will be visible. any rows for which it returns false or null will not be visible to the user (in a SELECT), and will not be available for modification (in an UPDATE or DELETE). typically, such rows are silently suppressed โ€” no error is reported (but see Table 300 for exceptions, because there's always a Table 300).
  • check_expression โ€” any SQL conditional expression (returning boolean). again, the conditional expression cannot contain any aggregate or window functions. this expression gets used in INSERT and UPDATE queries against the table if row-level security is enabled. only rows for which the expression evaluates to true will be allowed. an error will be thrown if the expression evaluates to false or null for any of the records inserted, or any of the records that result from the update. also note: check_expression is evaluated against the proposed new contents of the row, not the original contents. she's looking forward, not back โœจ

๐ŸŒˆ Per-Command Policies

โญ ALL

Using ALL for a policy means it applies to all commands, regardless of the type of command. if an ALL policy exists and more specific policies exist, then both the ALL policy and the more specific policy (or policies) get applied. additionally, ALL policies get applied to both the selection side of a query and the modification side, using the USING expression for both cases if only a USING expression has been defined.

as an example: if an UPDATE is issued, then the ALL policy is applicable both to what the UPDATE will be able to select as rows to be updated (applying the USING expression), and to the resulting updated rows, to check if they're permitted to be added to the table (applying the WITH CHECK expression if defined, and the USING expression otherwise). and if an INSERT or UPDATE command tries to add rows to the table that don't pass the ALL policy's WITH CHECK expression (or its USING expression, if it doesn't have a WITH CHECK expression), the entire command will be aborted. all of it. gone. ๐Ÿ˜ญ

๐Ÿ’œ SELECT

Using SELECT for a policy means it applies to SELECT queries and whenever SELECT permissions are required on the relation the policy is defined for. the result? only those records from the relation that pass the SELECT policy get returned during a SELECT query, and queries that require SELECT permissions โ€” like UPDATE, DELETE, and MERGE โ€” will also only see those records that are allowed by the SELECT policy.

a SELECT policy cannot have a WITH CHECK expression, since it only applies in cases where records are being retrieved from the relation, except as described below. (there's always an "except as described below," iykyk)

if a data-modifying query has a RETURNING clause, SELECT permissions are required on the relation, and any newly inserted or updated rows from the relation must satisfy the relation's SELECT policies in order to be available to the RETURNING clause. if a newly inserted or updated row doesn't satisfy the relation's SELECT policies, an error will be thrown โ€” inserted or updated rows to be returned are never silently ignored. she said what she said ๐Ÿ’…

if an INSERT has an ON CONFLICT DO UPDATE clause, or an ON CONFLICT DO NOTHING clause with an arbiter index or constraint specification, then SELECT permissions are required on the relation, and the rows proposed for insertion are checked using the relation's SELECT policies. if a row proposed for insertion doesn't satisfy the relation's SELECT policies, an error is thrown (the INSERT is never silently avoided). in addition, if the UPDATE path is taken, the row to be updated and the new updated row are checked against the relation's SELECT policies, and an error is thrown if they're not satisfied (an auxiliary UPDATE is never silently avoided). no sneaky skips around here ๐Ÿ”ฅ

a MERGE command requires SELECT permissions on both the source and target relations, so each relation's SELECT policies are applied before they're joined, and the MERGE actions will only see those records that are allowed by those policies. in addition, if an UPDATE action is executed, the target relation's SELECT policies are applied to the updated row, just like for a standalone UPDATE โ€” except that an error is thrown if they're not satisfied. dramatic but fair โœจ

๐Ÿฆ‹ INSERT

Using INSERT for a policy means it applies to INSERT commands and MERGE commands that contain INSERT actions. rows being inserted that don't pass this policy will result in a policy violation error, and the entire INSERT command will be aborted.

an INSERT policy cannot have a USING expression, because it only applies in cases where records are being added to the relation. adding, not looking. periodt ๐Ÿ‘‘

and note: an INSERT with an ON CONFLICT DO NOTHING/UPDATE clause will check the INSERT policies' WITH CHECK expressions for all rows proposed for insertion, regardless of whether or not they end up actually being inserted. she checks everybody at the door, even the ones who leave ๐ŸŽ€

๐Ÿ‘‘ UPDATE

Using UPDATE for a policy means it applies to UPDATE, SELECT FOR UPDATE, and SELECT FOR SHARE commands, plus auxiliary ON CONFLICT DO UPDATE clauses of INSERT commands, and MERGE commands containing UPDATE actions. she gets around, honestly ๐Ÿ’…

since an UPDATE command involves pulling an existing record and replacing it with a new modified record, UPDATE policies accept both a USING expression and a WITH CHECK expression. the USING expression determines which records the UPDATE command will see to operate against, while the WITH CHECK expression defines which modified rows are allowed to be stored back into the relation.

any rows whose updated values don't pass the WITH CHECK expression will cause an error, and the entire command will be aborted. and if only a USING clause is specified, then that clause gets used for both the USING and WITH CHECK cases. one clause, double duty, we love a multitasker ๐ŸŒŸ

Table 300. Policies Applied by Command Type โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ SELECT/ALL โ”‚ INSERT/ALL โ”‚ UPDATE/ALL policy โ”‚ DELETE/ALL โ”‚ โ”‚ โ”‚ policy โ”‚ policy โ”‚ โ”‚ policy โ”‚ โ”‚ Command โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ USING โ”‚ WITH CHECK โ”‚ USING โ”‚ WITH CHECK โ”‚ USING โ”‚ โ”‚ โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ SELECT / โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ COPY ... TO โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ SELECT FOR โ”‚ Filter โ”‚ โ€” โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ UPDATE/SHARE โ”‚ existing โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ INSERT โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ โ”‚ row [a] โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ UPDATE โ”‚ Filter โ”‚ โ€” โ”‚ Filter โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ existing โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ row [a] & โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ check โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ new row [a] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ DELETE โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ Filter โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ row [a] โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ INSERT ... โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ ON CONFLICT โ”‚ row [b][c] โ”‚ row [c] โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ON CONFLICT โ”‚ Check โ”‚ โ€” โ”‚ Check โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ DO UPDATE โ”‚ existing & โ”‚ โ”‚ existing โ”‚ row [d] โ”‚ โ”‚ โ”‚ โ”‚ new rows [d] โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE โ”‚ Filter source โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ โ”‚ & target rows โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ THEN INSERT โ”‚ row [a] โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ Check new row โ”‚ โ€” โ”‚ Check โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ THEN UPDATE โ”‚ โ”‚ โ”‚ existing โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ Check โ”‚ โ”‚ THEN DELETE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ---- โ”‚ โ”‚ [a] If read access is required to either the existing or new row โ”‚ โ”‚ (for example, a WHERE or RETURNING clause that refers to columns โ”‚ โ”‚ from the relation). ---- โ”‚ โ”‚ [b] If an arbiter index or constraint is specified. ---- โ”‚ โ”‚ [c] Row proposed for insertion is checked regardless of whether or โ”‚ โ”‚ not a conflict occurs. ---- โ”‚ โ”‚ [d] New row of the auxiliary UPDATE command, which might be โ”‚ โ”‚ different from the new row of the original INSERT command. โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜


Wait, I see a potential issue: In the original, the line for "UPDATE" has an extra space before "check" in "         check". That is, it's "โ”‚              โ”‚         check โ”‚". I have that. Good.

Also, the line for "MERGE ... THEN UPDATE" has "โ”‚ Check new row โ”‚ โ€”          โ”‚ Check      โ”‚ Check new  โ”‚ โ€”          โ”‚". I have that.

Now, the bottom border line: `โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜`. I need to count the dashes. In the original, it's exactly as I copied. I'll trust it.

Now, I'll put this in a code block. I'll use ```text to start and ``` to end.

I need to ensure that the code block is not indented, because if I indent it, it might be rendered as a code block but with extra spaces. The instruction says "fenced code blocks (```) for synopsis lines, examples and multi-line code. Code is shown verbatim, so keep it exact." So I'll just put the fence at the beginning of a line, no indentation.

Now, let's assemble the final output. I'll write the prose, then the code block.

I should also add a little transition after the table? But the user said more parts follow, so no sign-off. I can just end with the code block. That's fine.

Now, check for any missed details. The original had a sentence before the table: "Table 300 summarizes how the different types of policy apply to specific commands. In the table, โ€œcheckโ€ means that the policy expression is checked and an error is thrown if it returns false or null, whereas โ€œfilterโ€ means that the row is silently ignored if the policy expression returns false or null." I included that. The original also had the "DELETE" heading. I included it. The original had the UPDATE paragraphs. I included them. All good.

Now, about the color tags: I used <pink>, <red>, <rainbow>. I need to close them. I did. I also used emoji.

One more thing: The instruction says "Don't put them inside backticks." I didn't. I put color tags around plain text, not inside backticks. For example, <red>**error will be thrown**</red> โ€” that's fine because the backticks are around `error will be thrown`? Wait, I wrote: an <red>**error will be thrown**</red>. There are no backticks there. So it's fine. I used **bold** inside the color tag. That's allowed? The instruction says color tags may wrap any inline text. Bold is inline text. So it's fine.

I used <rainbow>Table 300</rainbow> โ€” that's fine.

Now, final output.

I'll write it out.
</think>So like, an `UPDATE` command usually also needs to read data from the columns in the relation it's updatingโ€”like in a `WHERE` clause, or a `RETURNING` clause, or in an expression on the right-hand side of the `SET` clause. When that happens, you also need <pink>SELECT</pink> rights on the relation being updated, and the appropriate <pink>SELECT</pink> or <pink>ALL</pink> policies get applied **in addition to** the `UPDATE` policies. So basically the user has to have access to the row(s) being updated through a <pink>SELECT</pink> or <pink>ALL</pink> policy, AND be granted permission to update the row(s) via an `UPDATE` or <pink>ALL</pink> policy. Both. No cap. ๐Ÿ’…

Now, when an `INSERT` command has an auxiliary `ON CONFLICT DO UPDATE` clause, and the `UPDATE` path is taken, the row to be updated is first checked against the `USING` expressions of any `UPDATE` policies. Then the new updated row is checked against the `WITH CHECK` expressions. BUTโ€”and this is the teaโ€”unlike a standalone `UPDATE` command, if the existing row does not pass the `USING` expressions, an <red>**error will be thrown**</red>. The `UPDATE` path will never be silently avoided. Same energy for an `UPDATE` action of a `MERGE` command. ๐Ÿซ–

## DELETE ๐Ÿ—‘๏ธ

Using `DELETE` for a policy means it applies to `DELETE` commands and `MERGE` commands containing `DELETE` actions. For a `DELETE` command, only rows that pass this policy will be seen by the `DELETE` command. There can be rows that are visible through a `SELECT` policy that are **not** available for deletion, if they do not pass the `USING` expression for the `DELETE` policy. But heads up: a `DELETE` action in a `MERGE` command will see rows that are visible through `SELECT` policies, and if the `DELETE` policy does not pass for such a row, an <red>**error will be thrown**</red>. ๐Ÿ˜ญ

In most cases, a `DELETE` command also needs to read data from columns in the relation that it is deleting from (e.g., in a `WHERE` clause or a `RETURNING` clause). In this case, `SELECT` rights are also required on the relation, and the appropriate `SELECT` or `ALL` policies will be applied **in addition to** the `DELETE` policies. So the user must have access to the row(s) being deleted through a `SELECT` or `ALL` policy, AND be granted permission to delete the row(s) via a `DELETE` or `ALL` policy. Both, bestie. ๐Ÿซถ

A `DELETE` policy **cannot** have a `WITH CHECK` expression, because it only applies in cases where records are being deleted from the relationโ€”so there is no new row to check. Periodt. ๐Ÿ’…

<rainbow>Table 300</rainbow> summarizes how the different types of policy apply to specific commands. In the table, โ€œcheckโ€ means that the policy expression is checked and an error is thrown if it returns false or null, whereas โ€œfilterโ€ means that the row is silently ignored if the policy expression returns false or null. No drama, just skipped. โœจ

Table 300. Policies Applied by Command Type โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ SELECT/ALL โ”‚ INSERT/ALL โ”‚ UPDATE/ALL policy โ”‚ DELETE/ALL โ”‚ โ”‚ โ”‚ policy โ”‚ policy โ”‚ โ”‚ policy โ”‚ โ”‚ Command โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ USING โ”‚ WITH CHECK โ”‚ USING โ”‚ WITH CHECK โ”‚ USING โ”‚ โ”‚ โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ expression โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ SELECT / โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ COPY ... TO โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ SELECT FOR โ”‚ Filter โ”‚ โ€” โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ UPDATE/SHARE โ”‚ existing โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ INSERT โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ โ”‚ row [a] โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ UPDATE โ”‚ Filter โ”‚ โ€” โ”‚ Filter โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ existing โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ row [a] & โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ check โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ new row [a] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ DELETE โ”‚ Filter โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ Filter โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ row [a] โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ INSERT ... โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ ON CONFLICT โ”‚ row [b][c] โ”‚ row [c] โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ON CONFLICT โ”‚ Check โ”‚ โ€” โ”‚ Check โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ DO UPDATE โ”‚ existing & โ”‚ โ”‚ existing โ”‚ row [d] โ”‚ โ”‚ โ”‚ โ”‚ new rows [d] โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE โ”‚ Filter source โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ โ”‚ & target rows โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ Check new โ”‚ Check new โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ”‚ THEN INSERT โ”‚ row [a] โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ Check new row โ”‚ โ€” โ”‚ Check โ”‚ Check new โ”‚ โ€” โ”‚ โ”‚ THEN UPDATE โ”‚ โ”‚ โ”‚ existing โ”‚ row โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ MERGE ... โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ โ€” โ”‚ Check โ”‚ โ”‚ THEN DELETE โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ existing โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ row โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ---- โ”‚ โ”‚ [a] If read access is required to either the existing or new row โ”‚ โ”‚ (for example, a WHERE or RETURNING clause that refers to columns โ”‚ โ”‚ from the relation). ---- โ”‚ โ”‚ [b] If an arbiter index or constraint is specified. ---- โ”‚ โ”‚ [c] Row proposed for insertion is checked regardless of whether or โ”‚ โ”‚ not a conflict occurs. ---- โ”‚ โ”‚ [d] New row of the auxiliary UPDATE command, which might be โ”‚ โ”‚ different from the new row of the original INSERT command. โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜


## ๐Ÿฆ‹ Application of Multiple Policies ๐Ÿฆ‹

Okay so this is where it gets <hotpink>spicy</hotpink> and you actually need to lock in, bestie.

When you've got multiple policies of **different command types** all applying to the *same* command โ€” like a `SELECT` policy and an `UPDATE` policy both showing up for an `UPDATE` command โ€” then the user has to have **both** types of permissions. Like, yes you can select those rows *and* yes you can update them. So the expressions for one type get smooshed together with the expressions for the other type using the `AND` operator. <purple>Both. Always. No skipping.</purple>

Now when it's multiple policies of the **same command type** hitting the same command, the rules change up:

- there must be **at least one `PERMISSIVE` policy** granting access to the relation, and
- **all** the `RESTRICTIVE` policies must pass.

So: all the `PERMISSIVE` policy expressions get combined with `OR`, all the `RESTRICTIVE` policy expressions get combined with `AND`, and then those two results get combined with `AND`. ๐Ÿ’…

> And if there are **no** `PERMISSIVE` policies at all? Access is **denied**. Full stop. Periodt.

Also super important: for the purposes of combining multiple policies, <cyan>**`ALL` policies are treated as having the same type as whichever other type of policy is being applied**</cyan>. iykyk.

So for example, in an `UPDATE` command requiring both `SELECT` and `UPDATE` permissions, if there are multiple applicable policies of each type, they get combined like this:

expression from RESTRICTIVE SELECT/ALL policy 1 AND expression from RESTRICTIVE SELECT/ALL policy 2 AND ... AND ( expression from PERMISSIVE SELECT/ALL policy 1 OR expression from PERMISSIVE SELECT/ALL policy 2 OR ... ) AND expression from RESTRICTIVE UPDATE/ALL policy 1 AND expression from RESTRICTIVE UPDATE/ALL policy 2 AND ... AND ( expression from PERMISSIVE UPDATE/ALL policy 1 OR expression from PERMISSIVE UPDATE/ALL policy 2 OR ... )


Look at all those parentheses. <yellow>She's structured. She's organized. She ate.</yellow> โœจ

---

# ๐Ÿ“ NOTES ๐Ÿ“

First things first, bestie: <hotpink>you must be the **owner of a table** to create or change policies for it.</hotpink> Not a vibe, just a fact.

So here's a thing that lowkey matters a lot. Policies get applied for explicit queries against tables in the database, **but they are not applied** when the system is doing internal referential integrity checks or validating constraints. ๐Ÿ˜ญ Which means there are *indirect* ways to figure out that a given value exists. Sneaky!!

An example of this: you try to insert a duplicate value into a column that's a primary key or has a unique constraint. If the insert **fails**, then the user can infer that the value already exists. (This example assumes the user is permitted by policy to insert records which they are **not** allowed to see. Yeah. That.) ๐Ÿ™ˆ

Another example: a user is allowed to insert into a table which references another, otherwise hidden table. Existence can be determined by the user inserting values into the referencing table, where **success** would indicate that the value exists in the referenced table. <red>Leakage. Through the side door.</red>

These issues can be addressed by carefully crafting policies to prevent users from being able to insert, delete, or update records at all which might possibly indicate a value they are not otherwise able to see โ€” or by using generated values (e.g., surrogate keys) instead of keys with external meanings. ๐Ÿงš

Generally, the system will enforce filter conditions imposed using security policies **prior to** qualifications that appear in user queries, so that protected data doesn't get inadvertently exposed to user-defined functions which might not be trustworthy. <lavender>Safety first, always.</lavender>

However! Functions and operators marked by the system (or the system administrator) as <cyan>`LEAKPROOF`</cyan> may be evaluated **before** policy expressions, because they're assumed to be trustworthy. Trust the process, but only that process. ๐Ÿ’…

Since policy expressions are added to the user's query directly, they'll be run with the **rights of the user running the overall query**. So if you're using a given policy, you must be able to access any tables or functions referenced in the expression โ€” otherwise you'll just get a permission denied error when you try to query the table that has row-level security enabled. ๐Ÿ˜ญ

This does **not** change how views work, though. As with normal queries and views, permission checks and policies for the tables which are referenced by a view will use the **view owner's rights** and any policies which apply to the view owner โ€” except if the view is defined using the <cyan>`security_invoker`</cyan> option (see `CREATE VIEW`). ๐Ÿ‘‘

No separate policy exists for `MERGE`. Instead, the policies defined for `SELECT`, `INSERT`, `UPDATE`, and `DELETE` are applied while executing `MERGE`, depending on the actions that are performed. Main character energy: `MERGE` doesn't need its own policy, it borrows everyone else's. โœจ

Additional discussion and practical examples can be found in **Section 5.9**. Go read it, fr fr. ๐Ÿ“š

---

# ๐ŸŒˆ COMPATIBILITY ๐ŸŒˆ

`CREATE POLICY` is a <purple>PostgreSQL extension</purple>. That's it, that's the whole section. ๐Ÿซถ

---

# ๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

- `ALTER POLICY` (`ALTER_POLICY(7)`)
- `DROP POLICY` (`DROP_POLICY(7)`)
- `ALTER TABLE` (`ALTER_TABLE(7)`)

---

<pink>And that's the whole page, bestie โ€” you made it to the end!! ๐ŸŽ€ This one came from **PostgreSQL 18.6**, dated **2026**, and it's the `CREATE POLICY(7)` page. Go forth and secure your rows. ๐Ÿ’–โœจ</pink>