Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wCTs7-0021h1-2c for pgsql-hackers@arkaria.postgresql.org; Tue, 14 Apr 2026 02:51:28 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wCTr5-009SnG-1D for pgsql-hackers@arkaria.postgresql.org; Tue, 14 Apr 2026 02:50:24 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wCTr4-009Sn7-3A for pgsql-hackers@lists.postgresql.org; Tue, 14 Apr 2026 02:50:23 +0000 Received: from mail-pl1-x62d.google.com ([2607:f8b0:4864:20::62d]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wCTr3-00000000x7q-12Dx for pgsql-hackers@lists.postgresql.org; Tue, 14 Apr 2026 02:50:23 +0000 Received: by mail-pl1-x62d.google.com with SMTP id d9443c01a7336-2ab46931cf1so30749405ad.0 for ; Mon, 13 Apr 2026 19:50:20 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1776135019; x=1776739819; darn=lists.postgresql.org; h=references:to:cc:in-reply-to:date:subject:mime-version:message-id :from:from:to:cc:subject:date:message-id:reply-to; bh=l/7TsVsCM9xY+RhOH5X3X5ekffZTK5kxhK1PiFsw6P0=; b=nbx6SC8PpJo20tmI8MmnW9slJYMO/4oradZsu72E2OPaUhGGmE85SvkhJYtolV2DMn bXYxJzooCyC+ye6VfP8VAncPR5VFRXxmhOQl2SEZV46hiDxOwfUdwqrtU7P55nmln71H i3wLSEBGb3gnHb3HvitLns2FqIgvPQ/8uS5SCNxisgLYEoqIBpOJcgtRp0VmEXqHWBeW as1PTC1QeoulJ8J43wXIGpW0+jLgJNM77pr5F0i3aMpTQPWeVAtaKUX4vcxmeQgIxgZL r+8ZZ62beuiY5G+B7xzaOv+4J58tlrmpIyhzJBJysOarv5ZAey+3F+UnnKPgJNIAyJUd EaYw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776135019; x=1776739819; h=references:to:cc:in-reply-to:date:subject:mime-version:message-id :from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=l/7TsVsCM9xY+RhOH5X3X5ekffZTK5kxhK1PiFsw6P0=; b=g2QLWMW1opOd/hCLVZ4lOa5nc6MATGGi0Gf/jymwyL83PT2VIfShWjWmNyIMvEL4w6 cLf7Yoh+D7zwVbaBfP93yYXzgHExV2lNhQ66quoYIZwOsx1xD8CLYdcE+XiUEdfaaVIY Y9MS16UN2gnaxw70OAGpENF7lod+ci9QwEZsYTH3zfy5N56eapNtBnjdZETvJU1OEhGW p9MUroqTXQmSj5KpN3x4wO9Yo9gA3HgbQBoNzyBRmCcXPGq+sq4inJXJ5DkxT9TpFYmy 0dsfHy7NR08zqbCLSajNBSBkHvBCIx3nWGIl68+q8zfkKgWvkqwtgshdI674qeB7jOYc iHow== X-Forwarded-Encrypted: i=1; AFNElJ88NxPhr3EwHRllwAOqhRdq4CSPlyxuAoUN6UIJbhvoz46rjWHBwWdNwrHP2+8GJKVJxcLrO5+9+lAB+bvC@lists.postgresql.org X-Gm-Message-State: AOJu0YxfGb4CJr1C5oxovIPV4rSPIA1J87mvK3Xzfe7b8tlPNNgW157l 1TCWPjnRHb/S+V9LUOTlDCBbvIVXZNQNVgIjKsHqfQNPnYK0+Bhuur1i X-Gm-Gg: AeBDieuGhiVZ+sziBusYGytpPXzg/Ws7R/rxb+2Ww8TH05v44T6fnRN4IyKpKgdesqo Gv9jPVQUAQ9LqySCkbJvT+DReaJxbE+cBhZ4xwPVre6QpODZKO9zgqg8ZJpRVun5PQkqNExrPq/ y6be3SrseF6M00LHE4zGUcuzNt0A3V5vAuzEQ/PU/s99++CwvAKgV3L5UbW0e1Ru/UHU9BfjaYX /4A1YvQ3+ww/bM1v+b3PsJL4bgHNShs1IruVywkgQXvSjsMyITc1CULQrdO1WClHSBtcMytRxd3 OGEhA7vxSsji8mtkN5buRgwhUplHnvckXDorC4+UOIt2eC2iLqLxShYp6mQm71NOtQAl3h39k5G 8f1hx7z9rnRzTmBURGjHdffaoO69xTKHHxzEQoGfOoTi0225s/PnRrhT2J/b2z49iQXxiRfc0wm pcWzu9sD7Ypn9rW+OFX76sXulnAj7MWII= X-Received: by 2002:a17:902:8489:b0:2b2:ec33:cf15 with SMTP id d9443c01a7336-2b2ec33d1b3mr45994025ad.7.1776135018290; Mon, 13 Apr 2026 19:50:18 -0700 (PDT) Received: from smtpclient.apple ([45.32.121.103]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2b4612e60dasm41985705ad.38.2026.04.13.19.50.15 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Mon, 13 Apr 2026 19:50:17 -0700 (PDT) From: Chao Li Message-Id: <1FE6C83B-8CAB-4643-9D97-3F8C93C6B12F@gmail.com> Content-Type: multipart/mixed; boundary="Apple-Mail=_5EA9C5ED-CDDB-4480-BA2C-2E2744E20049" Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3864.400.21\)) Subject: Re: Bug: Rule actions see wrong values for generated columns (NEW.gen reads OLD value) Date: Tue, 14 Apr 2026 10:49:38 +0800 In-Reply-To: Cc: Richard Guo , SATYANARAYANA NARLAPURAM , PostgreSQL Hackers , Peter Eisentraut To: Dean Rasheed References: <22B4A33A-99F3-46F5-BE0C-426A9E1D9ABA@gmail.com> X-Mailer: Apple Mail (2.3864.400.21) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --Apple-Mail=_5EA9C5ED-CDDB-4480-BA2C-2E2744E20049 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 > On Apr 13, 2026, at 19:03, Dean Rasheed = wrote: >=20 > On Mon, 13 Apr 2026, 09:20 Richard Guo, = wrote: > On Mon, Apr 13, 2026 at 4:21=E2=80=AFPM Chao Li = wrote: > > I think the issue is that rewriteTargetListIU() removes generated = columns from the target list, as described by this comment: >=20 > > Later, when the rule action is rewritten, = ReplaceVarsFromTargetList() cannot find a target list entry for NEW.gen. = For UPDATE rules, the missing NEW column is handled with = REPLACEVARS_CHANGE_VARNO, so it falls back to referencing the original = target relation row, which gives the old value. >=20 > I came to the same conclusion. >=20 > > One possible fix is to build a new target list that adds generated = columns back when there are rules to fire. I tried the solution locally = with some quick and dirty code and it seems to fix both stored and = virtual generated columns for me. >=20 > I think a simpler fix might be to expand generated column references > in the NEW relation to their generation expressions before > ReplaceVarsFromTargetList resolves NEW references, so that the base > column Vars within the expressions can be correctly resolved. > Something like attached. >=20 > - Richard >=20 > One thing about that approach is that it leads to 2 full rewrites of = the rule action using ReplaceVarsFromTargetList(). I think that could be = avoided by using including generated column expressions in the = targetlist passed to ReplaceVarsFromTargetList() by rewriteRuleAction(). = I haven't tried it, but I imagine it could reuse some code from = expand_generated_columns_internal(). >=20 > Regards, > Dean I think Dean=E2=80=99s comment aligns with my implementation. My = implementation just adds generated columns back to target list when = needed, and pass the new target list to ReplaceVarsFromTargetList.=20 Please see the attached diff for my implementation: * The diff is based on Richard=E2=80=99s v1 patch. * As this diff is only to show my implementation rather than proposing a = patch, so the diff is not well polished, and I didn=E2=80=99t touch the = change to expand_generated_columns_internal of v1 patch. * The v1 patch has a typo in generated_virtual.sql where =E2=80=9CSTORED=E2= =80=9D should be =E2=80=9CVIRTUAL=E2=80=9D, I fixed that as it impacts = the tests. * I updated the test cases by adding one more column, so that the = generated column is built upon two columns. I am really new to this area, so please don=E2=80=99t be surprised if I = missed something or made something wrong. I am trying to learn by = resolving problems. Best regards, -- Chao Li (Evan) HighGo Software Co., Ltd. https://www.highgo.com/ --Apple-Mail=_5EA9C5ED-CDDB-4480-BA2C-2E2744E20049 Content-Disposition: attachment; filename=chao_impl.diff Content-Type: application/octet-stream; x-unix-mode=0644; name="chao_impl.diff" Content-Transfer-Encoding: 7bit diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 0ba9931b2fd..e7cf63cb47a 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -332,6 +332,57 @@ acquireLocksOnSubLinks(Node *node, acquireLocksOnSubLinks_context *context) return expression_tree_walker(node, acquireLocksOnSubLinks, context); } +static List * +add_generated_columns_to_targetlist(List *targetList, + CmdType commandType, + Relation target_relation, + RangeTblEntry *target_rte, + int target_rt_index, + int result_relation, + int nomatch_varno) +{ + TupleDesc tupdesc = RelationGetDescr(target_relation); + List *result; + + if (!(tupdesc->constr && + (tupdesc->constr->has_generated_stored || + tupdesc->constr->has_generated_virtual))) + return targetList; + + result = list_copy(targetList); + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + Node *expr; + TargetEntry *te; + + if (!attr->attgenerated) + continue; + + expr = build_generation_expression(target_relation, i + 1); + ChangeVarNodes(expr, 1, target_rt_index, 0); + expr = ReplaceVarsFromTargetList(expr, + target_rt_index, + 0, + target_rte, + targetList, + result_relation, + (commandType == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, + nomatch_varno, + NULL); + + te = makeTargetEntry((Expr *) expr, + i + 1, + pstrdup(NameStr(attr->attname)), + false); + result = lappend(result, te); + } + + return result; +} /* * rewriteRuleAction - @@ -645,24 +696,16 @@ rewriteRuleAction(Query *parsetree, { RangeTblEntry *new_rte = rt_fetch(new_varno, sub_action->rtable); Relation new_rel; + List *new_targetlist; - /* - * First, expand any generated column references in the NEW relation. - * This must happen before ReplaceVarsFromTargetList, because the - * query's target list won't contain entries for generated columns - * (they are removed by rewriteTargetListIU). Without this step, - * ReplaceVarsFromTargetList would fall through to - * REPLACEVARS_CHANGE_VARNO (for UPDATE) or - * REPLACEVARS_SUBSTITUTE_NULL (for INSERT), producing wrong results - * when the generated column depends on columns being modified. - */ new_rel = relation_open(new_rte->relid, NoLock); - sub_action = (Query *) - expand_generated_columns_internal((Node *) sub_action, - new_rel, new_varno, - new_rte, - sub_action->resultRelation, - true); + new_targetlist = add_generated_columns_to_targetlist(parsetree->targetList, + event, + new_rel, + new_rte, + new_varno, + sub_action->resultRelation, + current_varno); relation_close(new_rel, NoLock); sub_action = (Query *) @@ -670,7 +713,7 @@ rewriteRuleAction(Query *parsetree, new_varno, 0, new_rte, - parsetree->targetList, + new_targetlist, sub_action->resultRelation, (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 363bd7ccad5..be421b738bc 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -1600,20 +1600,22 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); -- rules -- NEW.b in a rule action should reflect the generated column's new value -CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule (a int, c int, b int GENERATED ALWAYS AS (a + c) STORED); CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); -INSERT INTO gtest_rule (a) VALUES (1); +INSERT INTO gtest_rule (a, c) VALUES (1, 5); UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET c = 7; SELECT * FROM gtest_rule_log; op | old_b | new_b -----+-------+------- - INS | | 2 - UPD | 2 | 20 -(2 rows) + INS | | 6 + UPD | 6 | 15 + UPD | 15 | 17 +(3 rows) DROP TABLE gtest_rule, gtest_rule_log; -- sanity check of system catalog diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 5bbd2f684ce..35b54f98ca1 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -1522,20 +1522,22 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); -- rules -- NEW.b in a rule action should reflect the generated column's new value -CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule (a int, c int, b int GENERATED ALWAYS AS (a + c) VIRTUAL); CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); -INSERT INTO gtest_rule (a) VALUES (1); +INSERT INTO gtest_rule (a, c) VALUES (1, 5); UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET c = 7; SELECT * FROM gtest_rule_log; op | old_b | new_b -----+-------+------- - INS | | 2 - UPD | 2 | 20 -(2 rows) + INS | | 6 + UPD | 6 | 15 + UPD | 15 | 17 +(3 rows) DROP TABLE gtest_rule, gtest_rule_log; -- sanity check of system catalog diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 3265a1e88c6..bda1fe760e8 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -800,14 +800,15 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); -- rules -- NEW.b in a rule action should reflect the generated column's new value -CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule (a int, c int, b int GENERATED ALWAYS AS (a + c) STORED); CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); -INSERT INTO gtest_rule (a) VALUES (1); +INSERT INTO gtest_rule (a, c) VALUES (1, 5); UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET c = 7; SELECT * FROM gtest_rule_log; DROP TABLE gtest_rule, gtest_rule_log; diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index cd1cd938975..b7c4c711e61 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -813,14 +813,15 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); -- rules -- NEW.b in a rule action should reflect the generated column's new value -CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule (a int, c int, b int GENERATED ALWAYS AS (a + c) VIRTUAL); CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); -INSERT INTO gtest_rule (a) VALUES (1); +INSERT INTO gtest_rule (a, c) VALUES (1, 5); UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET c = 7; SELECT * FROM gtest_rule_log; DROP TABLE gtest_rule, gtest_rule_log; --Apple-Mail=_5EA9C5ED-CDDB-4480-BA2C-2E2744E20049--