Thread: BUG #17811: Replacing an underlying view breaks OLD/NEW tuple when accessing it via upper-level view
BUG #17811: Replacing an underlying view breaks OLD/NEW tuple when accessing it via upper-level view
From
PG Bug reporting form
Date:
The following bug has been logged on the website: Bug reference: 17811 Logged by: Alexander Lakhin Email address: exclusion@gmail.com PostgreSQL version: 15.2 Operating system: Ubuntu 22.04 Description: I've discovered an issue with replacing a view when there is another updatable view defined on top of it and the new underlying view has more columns than the previous one. First example with triggers: CREATE TABLE t (id int, f1 text); CREATE VIEW v1 AS SELECT id FROM t; CREATE VIEW v2 AS SELECT * FROM v1; CREATE OR REPLACE VIEW v1 AS SELECT * FROM t; CREATE FUNCTION v1_trig_fn() RETURNS trigger AS $$ BEGIN RAISE NOTICE 'old.id: %, old.f1: %', old.id, old.f1; UPDATE t SET id=new.id WHERE id=old.id; RETURN new; END; $$ LANGUAGE plpgsql; CREATE TRIGGER v1_upd_trig INSTEAD OF UPDATE ON v1 FOR EACH ROW EXECUTE FUNCTION v1_trig_fn(); INSERT INTO t VALUES (1, 11), (2, 22); SELECT * FROM t; UPDATE v2 SET id = 3 WHERE id = 2; SELECT * FROM t; id | f1 ----+---- 1 | 11 2 | 22 (2 rows) NOTICE: old.id: 2, old.f1: <NULL> -- ??? UPDATE 1 id | f1 ----+---- 1 | 11 3 | 22 (2 rows) but if new v1 defined with security_barrier: CREATE OR REPLACE VIEW v1 WITH (security_barrier = true) AS SELECT * FROM t; ... NOTICE: old.id: 2, old.f1: 22, old.f2: 222 -- OK but on master (after 47bb9db75) ... NOTICE: old.id: 2, old.f1: 22, old.f2: 222 WARNING: problem in alloc set MessageContext: req size > alloc size for chunk 0x62900003ccb0 in block 0x62900003c200 WARNING: problem in alloc set MessageContext: req size > alloc size for chunk 0x62900003ccb0 in block 0x62900003c200 (I've seen also runtime errors detected by asan.) Second example with rules (REL_15_STABLE without asserts): CREATE TABLE t(id int, f1 int, f2 int, f3 int, f4 int); CREATE TABLE tc(id int, f int); CREATE VIEW v1 AS SELECT 1 AS id; CREATE VIEW v2 AS SELECT * FROM v1; CREATE OR REPLACE VIEW v1 WITH (security_barrier = true) AS SELECT * FROM t; CREATE RULE v1_upd_rule AS ON UPDATE TO v1 DO INSTEAD (INSERT INTO tc VALUES (NEW.id, NEW.f1); UPDATE t SET id = NEW.id WHERE id = OLD.id;); INSERT INTO t VALUES (1, 11, 111, 1111, 11111), (2, 22, 222, 2222, 22222); SELECT * FROM t; SELECT * FROM v2; UPDATE v2 SET id = 3 WHERE id = 2; SELECT * FROM t; SELECT * FROM tc; id ---- 1 2 (2 rows) server closed the connection unexpectedly ... Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000555b19cd22b6 in bms_add_members (a=0x8, b=b@entry=0x555b1a645990) at bitmapset.c:808 808 if (a->nwords < b->nwords) (gdb) bt #0 0x0000555b19cd22b6 in bms_add_members (a=0x8, b=b@entry=0x555b1a645990) at bitmapset.c:808 #1 0x0000555b19d29273 in add_vars_to_targetlist (root=0x555b1a645088, vars=0x555b1a645940, where_needed=0x555b1a645990, create_new_ph=true) at initsplan.c:259 #2 0x0000555b19d2931d in build_base_rel_tlists (root=0x555b1a645088, final_tlist=<optimized out>) at initsplan.c:192 #3 0x0000555b19d2be26 in query_planner (root=root@entry=0x555b1a645088, qp_callback=qp_callback@entry=0x555b19d2c430 <standard_qp_callback>, qp_extra=qp_extra@entry=0x7fffd186cd10) at planmain.c:178 ... But with: CREATE RULE v1_upd_rule AS ON UPDATE TO v1 DO INSTEAD (INSERT INTO tc VALUES (NEW.id, NEW.f3); UPDATE t SET id = NEW.id WHERE id = OLD.id;); ... id ---- 1 2 (2 rows) UPDATE 1 id | f1 | f2 | f3 | f4 ----+----+-----+------+------- 1 | 11 | 111 | 1111 | 11111 3 | 22 | 222 | 2222 | 22222 (2 rows) id | f ----+------ 3 | 2222 (1 row) (Maybe it succeeded accidentally.) REL_15_STABLE with asserts: TRAP: FailedAssertion("attno >= rel->min_attr && attno <= rel->max_attr", File: "initsplan.c", Line: 249, PID: 266170) Interestingly enough, but replacing an underlying view with incompatible fields is prohibited (thanks to a check in checkViewTupleDesc()): CREATE VIEW v1 AS SELECT 1::text AS id; CREATE VIEW v2 AS SELECT * FROM v1; CREATE OR REPLACE VIEW v1 WITH (security_barrier = true) AS SELECT * FROM t; ERROR: cannot change data type of view column "id" from text to integer Just as decreasing of a number of columns: CREATE VIEW v1 AS SELECT 1 AS id, 11 AS f1, 111 AS f2, 1111 AS f3, 11111 AS f4, 111111 AS f5; CREATE VIEW v2 AS SELECT * FROM v1; CREATE OR REPLACE VIEW v1 WITH (security_barrier = true) AS SELECT * FROM t; ERROR: cannot drop columns from view Maybe replacing a view with increasing number of columns should be prohibited too?
Re: BUG #17811: Replacing an underlying view breaks OLD/NEW tuple when accessing it via upper-level view
From
Tom Lane
Date:
PG Bug reporting form <noreply@postgresql.org> writes: > Maybe replacing a view with increasing number of columns should be > prohibited too? It's intentional that we allow that case; it'd be sad if we have to remove the feature because of bugs. regards, tom lane
Re: BUG #17811: Replacing an underlying view breaks OLD/NEW tuple when accessing it via upper-level view
From
Tom Lane
Date:
PG Bug reporting form <noreply@postgresql.org> writes: > I've discovered an issue with replacing a view when there is another > updatable view defined on top of it and the new underlying view has > more columns than the previous one. I've looked into this a bit more, and both of these symptoms reduce to the same thing: after we've plugged the modified view's query into the upper query, we have an RTE_SUBQUERY RTE whose eref->colnames list is shorter than the number of columns actually available from the subquery. This breaks assorted code that is expecting that it can use list_length(eref->colnames) as a quick guide to the number of output columns. We have seen this before (bug #14876, commit d5b760ecb) and at the time our response was to patch up the code making such an assumption. But you've just demonstrated at least two other places with the same issue, so now I'm feeling that we'd be best advised to fix it centrally in the rewriter code that plugs in the view definition. The attached fix causes a change in the regression test output for bug #14876's test case, but AFAICS the change is a strict improvement: we get something sane, not a NULL, for the added column. I'm not sure whether to change the code added to expandRTE in d5b760ecb or leave it be. We could make it into elog(ERROR) now, perhaps. regards, tom lane diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index a614e3f5bd..980dc1816f 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -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" @@ -1730,6 +1731,7 @@ ApplyRetrieveRule(Query *parsetree, Query *rule_action; RangeTblEntry *rte; RowMarkClause *rc; + int numCols; if (list_length(rule->actions) != 1) elog(ERROR, "expected just one rule action"); @@ -1855,6 +1857,20 @@ ApplyRetrieveRule(Query *parsetree, rte->tablesample = NULL; rte->inh = false; /* must not be set for a subquery */ + /* + * 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; } diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 97bfc3475b..5e5b31f6ed 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -2551,16 +2551,16 @@ 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;
Re: BUG #17811: Replacing an underlying view breaks OLD/NEW tuple when accessing it via upper-level view
From
Alexander Lakhin
Date:
07.03.2023 21:43, Tom Lane wrote: > I've looked into this a bit more, and both of these symptoms reduce > to the same thing: after we've plugged the modified view's query > into the upper query, we have an RTE_SUBQUERY RTE whose eref->colnames > list is shorter than the number of columns actually available from the > subquery. This breaks assorted code that is expecting that it can > use list_length(eref->colnames) as a quick guide to the number of > output columns. Thanks for the fix! I've tested all my cases on REL_15_STABLE, master and found no anomalies in this area. Best regards, Alexander