Summary. After semantic analysis produces a Query tree, the rewrite system transforms it further by expanding views into their underlying queries, applying user-defined rules (CREATE RULE), and injecting row-level security (RLS) policy qualifiers. A single input Query can become zero, one, or many output Queries. The rewriter is the last transformation before the optimizer, and it operates entirely on Query trees – it never looks at raw parse trees or plan trees.
PostgreSQL’s rewrite system has its roots in the original POSTGRES project’s rule system – a mechanism for defining derived data and automatic actions. Today it serves three primary purposes:
View expansion. Views are implemented as ON SELECT DO INSTEAD rules. When a query references a view, the rewriter replaces the view’s range table entry with the view’s defining subquery.
User-defined rules. CREATE RULE attaches actions to INSERT, UPDATE, DELETE, or SELECT on a table. Rules can be INSTEAD (replacing the original action) or ALSO (adding actions alongside).
Row-level security (RLS). When RLS is enabled on a table, the rewriter injects policy-defined security qualifiers as additional WHERE conditions and WITH CHECK constraints.
The entry point is QueryRewrite() in src/backend/rewrite/rewriteHandler.c.
| File | Purpose |
|---|---|
src/backend/rewrite/rewriteHandler.c |
Main rewrite logic: QueryRewrite(), fireRIRrules(), rule matching |
src/backend/rewrite/rewriteManip.c |
Tree manipulation: variable renumbering, subquery adjustments |
src/backend/rewrite/rewriteDefine.c |
CREATE RULE / DROP RULE implementation |
src/backend/rewrite/rewriteSupport.c |
Catalog lookups for rules |
src/backend/rewrite/rewriteSearchCycle.c |
SEARCH/CYCLE clause handling for recursive CTEs |
src/backend/rewrite/rowsecurity.c |
RLS policy retrieval and injection |
src/backend/rewrite/rewriteRemove.c |
Rule removal |
src/include/rewrite/rewriteHandler.h |
Public API |
src/include/rewrite/rowsecurity.h |
RLS enums and function declarations |
/* src/backend/rewrite/rewriteHandler.c */
List *
QueryRewrite(Query *parsetree)
{
List *querylist;
List *results = NIL;
/* Step 1: Apply non-SELECT rules (INSERT/UPDATE/DELETE) */
querylist = RewriteQuery(parsetree, NIL, 0);
foreach(lc, querylist)
{
Query *query = (Query *) lfirst(lc);
/* Step 2: Apply RIR (Retrieve-Instead-Retrieve) rules */
/* This is where views get expanded */
query = fireRIRrules(query, NIL);
results = lappend(results, query);
}
return results;
}
The two-step structure is important: non-SELECT rules are fired first (potentially creating new queries), then view expansion runs on each resulting query.
Views are stored as ON SELECT DO INSTEAD rules in pg_rewrite. When fireRIRrules() encounters an RTE_RELATION in the range table, it checks for rules:
flowchart TD
A["fireRIRrules(query)"] --> B["For each RTE in query.rtable"]
B --> C{"RTE is a relation<br/>with SELECT rules?"}
C -->|No| B
C -->|Yes| D["Fetch view definition<br/>from pg_rewrite"]
D --> E["Adjust variable numbering<br/>(rewriteManip.c)"]
E --> F["Replace RTE_RELATION with<br/>RTE_SUBQUERY containing<br/>view's Query"]
F --> G{"View has<br/>security_barrier?"}
G -->|Yes| H["Mark RTE as<br/>security_barrier = true"]
G -->|No| I["Continue"]
H --> I
I --> J["Recursively fire RIR rules<br/>on the subquery"]
J --> B
What actually happens during expansion:
Var references in the view’s query are adjusted to account for merging into the outer query’s range table.RTE_RELATION to RTE_SUBQUERY, with the subquery field set to the view’s defining query.relid, relkind, rellockmode, and perminfoindex are preserved on the RTE so that runtime permission checks still apply to the view.security_barrier, the RTE is flagged accordingly, which prevents the optimizer from pushing external WHERE conditions below the view boundary.Recursive expansion. Views can reference other views. fireRIRrules() tracks active expansions to detect circular view definitions and raises an error if recursion is found.
Beyond views, PostgreSQL supports general-purpose rules via CREATE RULE:
CREATE RULE notify_insert AS ON INSERT TO orders
DO ALSO NOTIFY order_channel;
CREATE RULE redirect_delete AS ON DELETE TO archived_orders
DO INSTEAD
UPDATE archived_orders SET deleted_at = now()
WHERE archived_orders.id = OLD.id;
Rule types:
| Rule Kind | Behavior |
|---|---|
INSTEAD |
Replaces the original command entirely |
ALSO |
Adds actions in addition to the original command |
INSTEAD NOTHING |
Suppresses the original command with no replacement |
Rule application in RewriteQuery():
pg_rewrite.INSTEAD and ALSO categories.INSTEAD rule exists, suppress the original query.OLD and NEW references with the appropriate values from the original query.Rule actions are stored as Query trees in pg_rewrite.ev_action. They use special variables OLD (reference to the row before modification) and NEW (reference to the row after modification), which the rewriter substitutes with appropriate expressions during expansion.
When a view is “simple enough” (single table, no aggregates, no set operations, no DISTINCT, etc.), PostgreSQL makes it automatically updatable. INSERT/UPDATE/DELETE on such a view are rewritten to operate on the underlying table:
WITH CHECK OPTION is defined, a WithCheckOption node is added to ensure modified rows still satisfy the view’s WHERE condition.RLS policies are applied during rewriting by get_row_security_policies() in rowsecurity.c. This function is called from within fireRIRrules() for each RTE that has RLS enabled.
Policy structure:
CREATE POLICY employee_isolation ON employees
USING (department_id = current_setting('app.department_id')::int)
WITH CHECK (department_id = current_setting('app.department_id')::int);
Policy types:
| Type | Behavior |
|---|---|
| Permissive | Combined with OR – any permissive policy can grant access |
| Restrictive | Combined with AND – all restrictive policies must be satisfied |
Injection process:
flowchart TD
A["get_row_security_policies()"] --> B["check_enable_rls()<br/>Should RLS apply?"]
B -->|RLS_NONE| Z["Return (superuser/owner bypass)"]
B -->|RLS_ENABLED| C["get_policies_for_relation()<br/>Fetch from pg_policy"]
C --> D["Separate permissive<br/>and restrictive policies"]
D --> E["add_security_quals()<br/>Build WHERE filter"]
E --> F["Permissive quals<br/>OR'd together"]
F --> G["Restrictive quals<br/>AND'd with permissive result"]
G --> H["Prepend to RTE's<br/>securityQuals list"]
D --> I["add_with_check_options()<br/>For INSERT/UPDATE"]
I --> J["Build WithCheckOption list<br/>on Query"]
H --> K["Set query.hasRowSecurity = true"]
J --> K
The security quals are added to RangeTblEntry.securityQuals, which the optimizer treats as mandatory filter conditions that must be evaluated before any user-supplied conditions (to prevent information leakage through side-channel attacks like error messages from user functions).
RLS bypass rules:
ALTER TABLE ... FORCE ROW LEVEL SECURITY).row_security GUC can be set to off to raise an error if any RLS policy would apply (useful for ensuring applications do not accidentally rely on RLS).typedef struct RewriteRule
{
Oid ruleId;
CmdType event; /* SELECT, INSERT, UPDATE, DELETE */
Node *qual; /* rule condition, or NULL */
List *actions; /* list of Query trees */
char enabled; /* rule firing control */
bool isInstead; /* INSTEAD or ALSO */
} RewriteRule;
After rewriting, each Query carries a querySource tag indicating its origin:
typedef enum QuerySource
{
QSRC_ORIGINAL, /* the original user query */
QSRC_PARSER, /* added by parse analysis (unused) */
QSRC_INSTEAD_RULE, /* from an unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* from a conditional INSTEAD rule */
QSRC_NON_INSTEAD_RULE, /* from an ALSO rule */
} QuerySource;
For CHECK OPTION on views and RLS WITH CHECK clauses:
typedef struct WithCheckOption
{
NodeTag type;
WCOKind kind; /* WCO_VIEW_CHECK, WCO_RLS_INSERT_CHECK, etc. */
char *relname; /* name of relation (for error messages) */
char *polname; /* name of policy (for error messages) */
Node *qual; /* the check expression */
bool cascaded; /* cascaded CHECK OPTION? */
} WithCheckOption;
The securityQuals field on RangeTblEntry holds a list of security-barrier conditions. The executor evaluates these in a separate, inner qual context to prevent predicate pushdown from bypassing them:
/* Within RangeTblEntry */
List *securityQuals; /* security barrier quals for this RTE */
flowchart TD
Q["Input Query"] --> RQ["RewriteQuery()"]
subgraph "Non-SELECT Rule Processing"
RQ --> ML["matchLocks()<br/>Find matching rules"]
ML --> IR{"INSTEAD rules?"}
IR -->|Yes| SUP["Suppress original query"]
IR -->|No| KEEP["Keep original query"]
SUP --> ALSO["Add ALSO rule actions"]
KEEP --> ALSO
ALSO --> QL["Query list"]
end
QL --> EACH["For each Query in list"]
subgraph "RIR Processing (per query)"
EACH --> RIR["fireRIRrules()"]
RIR --> VE["Expand views to subqueries"]
VE --> RLS["Inject RLS security quals"]
RLS --> REC["Recurse into subqueries"]
end
REC --> OUT["Rewritten Query list"]
OUT --> OPT["To Optimizer"]
Security barrier views. A view created with
WITH (security_barrier)prevents the optimizer from pushing user-supplied WHERE conditions below the view boundary. Without this, a user-defined function in a WHERE clause could be evaluated on rows that the view is supposed to hide, leaking information through side effects. RLS-generated subqueries are always marked as security barriers.
Rule ordering. When multiple rules match, they fire in alphabetical order by rule name. The
_RETURNrule (the view definition) is always the only SELECT rule allowed on a relation.
Rules vs. triggers. Rules rewrite the query at parse time; triggers fire at execution time. For most DML side-effects, triggers are preferred because they handle row-level operations correctly (rules operate on the query level and can produce surprising results with volatile functions or RETURNING clauses).
| Related Section | Relationship |
|---|---|
| Semantic Analysis | Produces the Query tree that the rewriter consumes |
| Lexer & Parser | Rule actions are stored as serialized raw parse trees, re-analyzed when loaded |
| Query Optimizer (Ch. 7) | Consumes the rewritten Query list; must respect security_barrier markings |
| Executor (Ch. 8) | Evaluates WithCheckOption quals at runtime |
| Locking (Ch. 5) | AcquireRewriteLocks() ensures schema stability during rewriting |
| Caches (Ch. 9) | Relcache stores pre-parsed rule actions; invalidation forces re-expansion |