Skip to content

Commit 4d288e3

Browse files
committed
Avoid rewriting data-modifying CTEs more than once.
Formerly, when updating an auto-updatable view, or a relation with rules, if the original query had any data-modifying CTEs, the rewriter would rewrite those CTEs multiple times as RewriteQuery() recursed into the product queries. In most cases that was harmless, because RewriteQuery() is mostly idempotent. However, if the CTE involved updating an always-generated column, it would trigger an error because any subsequent rewrite would appear to be attempting to assign a non-default value to the always-generated column. This could perhaps be fixed by attempting to make RewriteQuery() fully idempotent, but that looks quite tricky to achieve, and would probably be quite fragile, given that more generated-column-type features might be added in the future. Instead, fix by arranging for RewriteQuery() to rewrite each CTE exactly once (by tracking the number of CTEs already rewritten as it recurses). This has the advantage of being simpler and more efficient, but it does make RewriteQuery() dependent on the order in which rewriteRuleAction() joins the CTE lists from the original query and the rule action, so care must be taken if that is ever changed. Reported-by: Bernice Southey <bernice.southey@gmail.com> Author: Bernice Southey <bernice.southey@gmail.com> Author: Dean Rasheed <dean.a.rasheed@gmail.com> Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us> Reviewed-by: Kirill Reshke <reshkekirill@gmail.com> Discussion: https://postgr.es/m/CAEDh4nyD6MSH9bROhsOsuTqGAv_QceU_GDvN9WcHLtZTCYM1kA@mail.gmail.com Backpatch-through: 14
1 parent b497766 commit 4d288e3

File tree

3 files changed

+90
-5
lines changed

3 files changed

+90
-5
lines changed

src/backend/rewrite/rewriteHandler.c

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,10 @@ rewriteRuleAction(Query *parsetree,
592592
}
593593
}
594594

595-
/* OK, it's safe to combine the CTE lists */
595+
/*
596+
* OK, it's safe to combine the CTE lists. Beware that RewriteQuery
597+
* knows we concatenate the lists in this order.
598+
*/
596599
sub_action->cteList = list_concat(sub_action->cteList,
597600
copyObject(parsetree->cteList));
598601
/* ... and don't forget about the associated flags */
@@ -3673,9 +3676,13 @@ rewriteTargetView(Query *parsetree, Relation view)
36733676
* orig_rt_length is the length of the originating query's rtable, for product
36743677
* queries created by fireRules(), and 0 otherwise. This is used to skip any
36753678
* already-processed VALUES RTEs from the original query.
3679+
*
3680+
* num_ctes_processed is the number of CTEs at the end of the query's cteList
3681+
* that have already been rewritten, and must not be rewritten again.
36763682
*/
36773683
static List *
3678-
RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
3684+
RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length,
3685+
int num_ctes_processed)
36793686
{
36803687
CmdType event = parsetree->commandType;
36813688
bool instead = false;
@@ -3689,17 +3696,29 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
36893696
* First, recursively process any insert/update/delete statements in WITH
36903697
* clauses. (We have to do this first because the WITH clauses may get
36913698
* copied into rule actions below.)
3699+
*
3700+
* Any new WITH clauses from rule actions are processed when we recurse
3701+
* into product queries below. However, when recursing, we must take care
3702+
* to avoid rewriting a CTE query more than once (because expanding
3703+
* generated columns in the targetlist more than once would fail). Since
3704+
* new CTEs from product queries are added to the start of the list (see
3705+
* rewriteRuleAction), we just skip the last num_ctes_processed items.
36923706
*/
36933707
foreach(lc1, parsetree->cteList)
36943708
{
36953709
CommonTableExpr *cte = lfirst_node(CommonTableExpr, lc1);
36963710
Query *ctequery = castNode(Query, cte->ctequery);
3711+
int i = foreach_current_index(lc1);
36973712
List *newstuff;
36983713

3714+
/* Skip already-processed CTEs at the end of the list */
3715+
if (i >= list_length(parsetree->cteList) - num_ctes_processed)
3716+
break;
3717+
36993718
if (ctequery->commandType == CMD_SELECT)
37003719
continue;
37013720

3702-
newstuff = RewriteQuery(ctequery, rewrite_events, 0);
3721+
newstuff = RewriteQuery(ctequery, rewrite_events, 0, 0);
37033722

37043723
/*
37053724
* Currently we can only handle unconditional, single-statement DO
@@ -3758,6 +3777,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
37583777
errmsg("multi-statement DO INSTEAD rules are not supported for data-modifying statements in WITH")));
37593778
}
37603779
}
3780+
num_ctes_processed = list_length(parsetree->cteList);
37613781

37623782
/*
37633783
* If the statement is an insert, update, delete, or merge, adjust its
@@ -4120,7 +4140,8 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length)
41204140
newstuff = RewriteQuery(pt, rewrite_events,
41214141
pt == parsetree ?
41224142
orig_rt_length :
4123-
product_orig_rt_length);
4143+
product_orig_rt_length,
4144+
num_ctes_processed);
41244145
rewritten = list_concat(rewritten, newstuff);
41254146
}
41264147

@@ -4272,7 +4293,7 @@ QueryRewrite(Query *parsetree)
42724293
*
42734294
* Apply all non-SELECT rules possibly getting 0 or many queries
42744295
*/
4275-
querylist = RewriteQuery(parsetree, NIL, 0);
4296+
querylist = RewriteQuery(parsetree, NIL, 0, 0);
42764297

42774298
/*
42784299
* Step 2

src/test/regress/expected/with.out

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2798,6 +2798,47 @@ SELECT * FROM bug6051_3;
27982798
---
27992799
(0 rows)
28002800

2801+
-- check that recursive CTE processing doesn't rewrite a CTE more than once
2802+
-- (must not try to expand GENERATED ALWAYS IDENTITY columns more than once)
2803+
CREATE TEMP TABLE id_alw1 (i int GENERATED ALWAYS AS IDENTITY);
2804+
CREATE TEMP TABLE id_alw2 (i int GENERATED ALWAYS AS IDENTITY);
2805+
CREATE TEMP VIEW id_alw2_view AS SELECT * FROM id_alw2;
2806+
CREATE TEMP TABLE id_alw3 (i int GENERATED ALWAYS AS IDENTITY);
2807+
CREATE RULE id_alw3_ins AS ON INSERT TO id_alw3 DO INSTEAD
2808+
WITH t1 AS (INSERT INTO id_alw1 DEFAULT VALUES RETURNING i)
2809+
INSERT INTO id_alw2_view DEFAULT VALUES RETURNING i;
2810+
CREATE TEMP VIEW id_alw3_view AS SELECT * FROM id_alw3;
2811+
CREATE TEMP TABLE id_alw4 (i int GENERATED ALWAYS AS IDENTITY);
2812+
WITH t4 AS (INSERT INTO id_alw4 DEFAULT VALUES RETURNING i)
2813+
INSERT INTO id_alw3_view DEFAULT VALUES RETURNING i;
2814+
i
2815+
---
2816+
1
2817+
(1 row)
2818+
2819+
SELECT * from id_alw1;
2820+
i
2821+
---
2822+
1
2823+
(1 row)
2824+
2825+
SELECT * from id_alw2;
2826+
i
2827+
---
2828+
1
2829+
(1 row)
2830+
2831+
SELECT * from id_alw3;
2832+
i
2833+
---
2834+
(0 rows)
2835+
2836+
SELECT * from id_alw4;
2837+
i
2838+
---
2839+
1
2840+
(1 row)
2841+
28012842
-- check case where CTE reference is removed due to optimization
28022843
EXPLAIN (VERBOSE, COSTS OFF)
28032844
SELECT q1 FROM

src/test/regress/sql/with.sql

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,29 @@ COMMIT;
13271327

13281328
SELECT * FROM bug6051_3;
13291329

1330+
-- check that recursive CTE processing doesn't rewrite a CTE more than once
1331+
-- (must not try to expand GENERATED ALWAYS IDENTITY columns more than once)
1332+
CREATE TEMP TABLE id_alw1 (i int GENERATED ALWAYS AS IDENTITY);
1333+
1334+
CREATE TEMP TABLE id_alw2 (i int GENERATED ALWAYS AS IDENTITY);
1335+
CREATE TEMP VIEW id_alw2_view AS SELECT * FROM id_alw2;
1336+
1337+
CREATE TEMP TABLE id_alw3 (i int GENERATED ALWAYS AS IDENTITY);
1338+
CREATE RULE id_alw3_ins AS ON INSERT TO id_alw3 DO INSTEAD
1339+
WITH t1 AS (INSERT INTO id_alw1 DEFAULT VALUES RETURNING i)
1340+
INSERT INTO id_alw2_view DEFAULT VALUES RETURNING i;
1341+
CREATE TEMP VIEW id_alw3_view AS SELECT * FROM id_alw3;
1342+
1343+
CREATE TEMP TABLE id_alw4 (i int GENERATED ALWAYS AS IDENTITY);
1344+
1345+
WITH t4 AS (INSERT INTO id_alw4 DEFAULT VALUES RETURNING i)
1346+
INSERT INTO id_alw3_view DEFAULT VALUES RETURNING i;
1347+
1348+
SELECT * from id_alw1;
1349+
SELECT * from id_alw2;
1350+
SELECT * from id_alw3;
1351+
SELECT * from id_alw4;
1352+
13301353
-- check case where CTE reference is removed due to optimization
13311354
EXPLAIN (VERBOSE, COSTS OFF)
13321355
SELECT q1 FROM

0 commit comments

Comments
 (0)