Skip to content

Commit

Permalink
Fix more bugs caused by adding columns to the end of a view.
Browse files Browse the repository at this point in the history
If a view is defined atop another view, and then CREATE OR REPLACE
VIEW is used to add columns to the lower view, then when the upper
view's referencing RTE is expanded by ApplyRetrieveRule we will have
a subquery RTE with fewer eref->colnames than output columns.  This
confuses various code that assumes those lists are always in sync,
as they are in plain parser output.

We have seen such problems before (cf commit d5b760e), and now
I think the time has come to do what was speculated about in that
commit: let's make ApplyRetrieveRule synthesize some column names to
preserve the invariant that holds in parser output.  Otherwise we'll
be chasing this class of bugs indefinitely.  Moreover, it appears from
testing that this actually gives us better results in the test case
d5b760e added, and likely in other corner cases that we lack
coverage for.

In HEAD, I replaced d5b760e's hack to make expandRTE exit early with
an elog(ERROR) call, since the case is now presumably unreachable.
But it seems like changing that in back branches would bring more risk
than benefit, so there I just updated the comment.

Per bug #17811 from Alexander Lakhin.  Back-patch to all supported
branches.

Discussion: https://postgr.es/m/17811-d31686b78f0dffc9@postgresql.org
  • Loading branch information
tglsfdc committed Mar 7, 2023
1 parent ae48601 commit 76d2177
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 14 deletions.
17 changes: 11 additions & 6 deletions src/backend/parser/parse_relation.c
Expand Up @@ -2672,12 +2672,17 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
Assert(varattno == te->resno);

/*
* In scenarios where columns have been added to a view
* since the outer query was originally parsed, there can
* be more items in the subquery tlist than the outer
* query expects. We should ignore such extra column(s)
* --- compare the behavior for composite-returning
* functions, in the RTE_FUNCTION case below.
* In a just-parsed subquery RTE, rte->eref->colnames
* should always have exactly as many entries as the
* subquery has non-junk output columns. However, if the
* subquery RTE was created by expansion of a view,
* perhaps the subquery tlist could now have more entries
* than existed when the outer query was parsed. Such
* cases should now be prevented because ApplyRetrieveRule
* will extend the colnames list to match. But out of
* caution, we'll keep the code like this in the back
* branches: just ignore any columns that lack colnames
* entries.
*/
if (!aliasp_item)
break;
Expand Down
16 changes: 16 additions & 0 deletions src/backend/rewrite/rewriteHandler.c
Expand Up @@ -26,6 +26,7 @@
#include "catalog/dependency.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "executor/executor.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
Expand Down Expand Up @@ -1724,6 +1725,7 @@ ApplyRetrieveRule(Query *parsetree,
RangeTblEntry *rte,
*subrte;
RowMarkClause *rc;
int numCols;

if (list_length(rule->actions) != 1)
elog(ERROR, "expected just one rule action");
Expand Down Expand Up @@ -1883,6 +1885,20 @@ ApplyRetrieveRule(Query *parsetree,
rte->updatedCols = NULL;
rte->extraUpdatedCols = NULL;

/*
* Since we allow CREATE OR REPLACE VIEW to add columns to a view, the
* rule_action might emit more columns than we expected when the current
* query was parsed. Various places expect rte->eref->colnames to be
* consistent with the non-junk output columns of the subquery, so patch
* things up if necessary by adding some dummy column names.
*/
numCols = ExecCleanTargetListLength(rule_action->targetList);
while (list_length(rte->eref->colnames) < numCols)
{
rte->eref->colnames = lappend(rte->eref->colnames,
makeString(pstrdup("?column?")));
}

return parsetree;
}

Expand Down
43 changes: 36 additions & 7 deletions src/test/regress/expected/alter_table.out
Expand Up @@ -2547,22 +2547,51 @@ View definition:
FROM at_view_1 v1;

explain (verbose, costs off) select * from at_view_2;
QUERY PLAN
----------------------------------------------------------------
QUERY PLAN
-------------------------------------------------------------
Seq Scan on public.at_base_table bt
Output: bt.id, bt.stuff, to_json(ROW(bt.id, bt.stuff, NULL))
Output: bt.id, bt.stuff, to_json(ROW(bt.id, bt.stuff, 4))
(2 rows)

select * from at_view_2;
id | stuff | j
----+--------+----------------------------------------
23 | skidoo | {"id":23,"stuff":"skidoo","more":null}
id | stuff | j
----+--------+-------------------------------------
23 | skidoo | {"id":23,"stuff":"skidoo","more":4}
(1 row)

drop view at_view_2;
drop view at_view_1;
drop table at_base_table;
-- check adding a column not iself requiring a rewrite, together with
-- related case (bug #17811)
begin;
create temp table t1 as select * from int8_tbl;
create temp view v1 as select 1::int8 as q1;
create temp view v2 as select * from v1;
create or replace temp view v1 with (security_barrier = true)
as select * from t1;
create temp table log (q1 int8, q2 int8);
create rule v1_upd_rule as on update to v1
do also insert into log values (new.*);
update v2 set q1 = q1 + 1 where q1 = 123;
select * from t1;
q1 | q2
------------------+-------------------
4567890123456789 | 123
4567890123456789 | 4567890123456789
4567890123456789 | -4567890123456789
124 | 456
124 | 4567890123456789
(5 rows)

select * from log;
q1 | q2
-----+------------------
124 | 456
124 | 4567890123456789
(2 rows)

rollback;
-- check adding a column not itself requiring a rewrite, together with
-- a column requiring a default (bug #16038)
-- ensure that rewrites aren't silently optimized away, removing the
-- value of the test
Expand Down
20 changes: 19 additions & 1 deletion src/test/regress/sql/alter_table.sql
Expand Up @@ -1632,7 +1632,25 @@ drop view at_view_2;
drop view at_view_1;
drop table at_base_table;

-- check adding a column not iself requiring a rewrite, together with
-- related case (bug #17811)
begin;
create temp table t1 as select * from int8_tbl;
create temp view v1 as select 1::int8 as q1;
create temp view v2 as select * from v1;
create or replace temp view v1 with (security_barrier = true)
as select * from t1;

create temp table log (q1 int8, q2 int8);
create rule v1_upd_rule as on update to v1
do also insert into log values (new.*);

update v2 set q1 = q1 + 1 where q1 = 123;

select * from t1;
select * from log;
rollback;

-- check adding a column not itself requiring a rewrite, together with
-- a column requiring a default (bug #16038)

-- ensure that rewrites aren't silently optimized away, removing the
Expand Down

0 comments on commit 76d2177

Please sign in to comment.