]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Avoid trying to lock OLD/NEW in a rule with FOR UPDATE.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 19 Aug 2021 16:12:36 +0000 (12:12 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 19 Aug 2021 16:12:36 +0000 (12:12 -0400)
transformLockingClause neglected to exclude the pseudo-RTEs for
OLD/NEW when processing a rule's query.  This led to odd errors
or even crashes later on.  This bug is very ancient, but it's
not terribly surprising that nobody noticed, since the use-case
for SELECT FOR UPDATE in a non-view rule is somewhere between
thin and non-existent.  Still, crashing is not OK.

Per bug #17151 from Zhiyong Wu.  Thanks to Masahiko Sawada
for analysis of the problem.

Discussion: https://postgr.es/m/17151-c03a3e6e4ec9aadb@postgresql.org

src/backend/parser/analyze.c
src/include/nodes/parsenodes.h
src/test/regress/expected/rules.out
src/test/regress/sql/rules.sql

index b903b89466ca59b6fc5bf294a830cf73784055f2..b61eeb9e5c8cfff85afac4863ce1d579c70aacbf 100644 (file)
@@ -2631,13 +2631,22 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
 
        if (lockedRels == NIL)
        {
-               /* all regular tables used in query */
+               /*
+                * Lock all regular tables used in query and its subqueries.  We
+                * examine inFromCl to exclude auto-added RTEs, particularly NEW/OLD
+                * in rules.  This is a bit of an abuse of a mostly-obsolete flag, but
+                * it's convenient.  We can't rely on the namespace mechanism that has
+                * largely replaced inFromCl, since for example we need to lock
+                * base-relation RTEs even if they are masked by upper joins.
+                */
                i = 0;
                foreach(rt, qry->rtable)
                {
                        RangeTblEntry *rte = (RangeTblEntry *) lfirst(rt);
 
                        ++i;
+                       if (!rte->inFromCl)
+                               continue;
                        switch (rte->rtekind)
                        {
                                case RTE_RELATION:
@@ -2667,7 +2676,11 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
        }
        else
        {
-               /* just the named tables */
+               /*
+                * Lock just the named tables.  As above, we allow locking any base
+                * relation regardless of alias-visibility rules, so we need to
+                * examine inFromCl to exclude OLD/NEW.
+                */
                foreach(l, lockedRels)
                {
                        RangeVar   *thisrel = (RangeVar *) lfirst(l);
@@ -2688,6 +2701,8 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
                                RangeTblEntry *rte = (RangeTblEntry *) lfirst(rt);
 
                                ++i;
+                               if (!rte->inFromCl)
+                                       continue;
                                if (strcmp(rte->eref->aliasname, thisrel->relname) == 0)
                                {
                                        switch (rte->rtekind)
index 4dd2d4ecfb799ce4cf100177f94fe35a2c7da9a8..44bcca62616cf651bb4cd53db5154e55f2a795ea 100644 (file)
@@ -752,10 +752,10 @@ typedef struct XmlSerialize
  *       inFromCl marks those range variables that are listed in the FROM clause.
  *       It's false for RTEs that are added to a query behind the scenes, such
  *       as the NEW and OLD variables for a rule, or the subqueries of a UNION.
- *       This flag is not used anymore during parsing, since the parser now uses
- *       a separate "namespace" data structure to control visibility, but it is
- *       needed by ruleutils.c to determine whether RTEs should be shown in
- *       decompiled queries.
+ *       This flag is not used during parsing (except in transformLockingClause,
+ *       q.v.); the parser now uses a separate "namespace" data structure to
+ *       control visibility.  But it is needed by ruleutils.c to determine
+ *       whether RTEs should be shown in decompiled queries.
  *
  *       requiredPerms and checkAsUser specify run-time access permissions
  *       checks to be performed at query startup.  The user must have *all*
index 56b3e8ddc935751e9d7bf610b57f08295595c44d..aa95c1ff7f44945e039b854c885ab4db4c493d52 100644 (file)
@@ -2614,6 +2614,31 @@ select * from only t1_2;
 (10 rows)
 
 reset constraint_exclusion;
+-- test FOR UPDATE in rules
+create table rules_base(f1 int, f2 int);
+insert into rules_base values(1,2), (11,12);
+create rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 1 for update;
+update rules_base set f2 = f2 + 1;
+ f1 | f2 
+----+----
+  1 |  2
+(1 row)
+
+create or replace rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 11 for update of rules_base;
+update rules_base set f2 = f2 + 1;
+ f1 | f2 
+----+----
+ 11 | 12
+(1 row)
+
+create or replace rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 11 for update of old; -- error
+ERROR:  relation "old" in FOR UPDATE clause not found in FROM clause
+LINE 2:   select * from rules_base where f1 = 11 for update of old;
+                                                               ^
+drop table rules_base;
 -- test various flavors of pg_get_viewdef()
 select pg_get_viewdef('shoe'::regclass) as unpretty;
                     unpretty                    
index 945503d9153abb3293f2bd9fc683f42ac03478cd..7bdeb2f5d1c464ed9b8bca0c6d962bb06acd3ce1 100644 (file)
@@ -980,6 +980,20 @@ select * from only t1_2;
 
 reset constraint_exclusion;
 
+-- test FOR UPDATE in rules
+
+create table rules_base(f1 int, f2 int);
+insert into rules_base values(1,2), (11,12);
+create rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 1 for update;
+update rules_base set f2 = f2 + 1;
+create or replace rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 11 for update of rules_base;
+update rules_base set f2 = f2 + 1;
+create or replace rule r1 as on update to rules_base do instead
+  select * from rules_base where f1 = 11 for update of old; -- error
+drop table rules_base;
+
 -- test various flavors of pg_get_viewdef()
 
 select pg_get_viewdef('shoe'::regclass) as unpretty;