Thread: Violation of principle that plan trees are read-only

Violation of principle that plan trees are read-only

From
Tom Lane
Date:
While chasing down Valgrind leakage reports, I was disturbed
to realize that some of them arise from a case where the
executor scribbles on the plan tree it's given, which it is
absolutely not supposed to do:

        /*
         * Initialize result tuple slot and assign its rowtype using the first
         * RETURNING list.  We assume the rest will look the same.
         */
        mtstate->ps.plan->targetlist = (List *) linitial(returningLists);

A bit of git archaeology fingers Andres' commit 4717fdb14, which we
can't easily revert since he later got rid of ExecAssignResultType
altogether.  But I think we need to do something about it --- it's
purest luck that this doesn't cause serious problems in some cases.

            regards, tom lane



Re: Violation of principle that plan trees are read-only

From
Robert Haas
Date:
On Sun, May 18, 2025 at 7:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> While chasing down Valgrind leakage reports, I was disturbed
> to realize that some of them arise from a case where the
> executor scribbles on the plan tree it's given, which it is
> absolutely not supposed to do:
>
>         /*
>          * Initialize result tuple slot and assign its rowtype using the first
>          * RETURNING list.  We assume the rest will look the same.
>          */
>         mtstate->ps.plan->targetlist = (List *) linitial(returningLists);
>
> A bit of git archaeology fingers Andres' commit 4717fdb14, which we
> can't easily revert since he later got rid of ExecAssignResultType
> altogether.  But I think we need to do something about it --- it's
> purest luck that this doesn't cause serious problems in some cases.

Is there some way that we can detect violations of this rule
automatically? I recall that we were recently discussing with Richard
Guo a proposed patch that would have had a similar problem, so it's
evidently not that hard for a committer to either fail to understand
what the rule is or fail to realize that they are violating it.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: Violation of principle that plan trees are read-only

From
Robert Haas
Date:
On Mon, May 19, 2025 at 10:35 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> I proposed a possible way to test for this at [1].  I was intending to
> get around to that sooner or later, but the urgency of the matter just
> went up in my eyes...

Ah, right, I actually read that thread but had forgotten about it. I
don't know if that idea will work out but it certainly seems like a
good thing to try.

--
Robert Haas
EDB: http://www.enterprisedb.com



Re: Violation of principle that plan trees are read-only

From
Tom Lane
Date:
Isaac Morland <isaac.morland@gmail.com> writes:
> I assume this question has an obvious negative answer, but why can't we
> attach const declarations to the various structures that make up the plan
> tree (at all levels, all the way down)? I know const doesn't actually
> prevent a value from changing, but at least the compiler would complain if
> code accidentally tried.

The big problem is that a "const" attached to a top-level pointer
doesn't inherently propagate down to sub-nodes.  So if I had, say,
"const Query *stmt", the compiler would complain about

    stmt->jointree = foo;

but not about

    stmt->jointree->quals = foo;

I guess we could imagine developing an entirely parallel set of
struct declarations with "const" on all pointer fields, like

typedef struct ConstQuery
{
    ...
    const ConstFromExpr   *jointree;
    ...
} ConstQuery;

but even with automated maintenance of the ConstFoo doppelganger
typedefs, it seems like that'd be a notational nightmare.  For
one thing, I'm not sure how to teach the compiler that casting
"Query *" to "ConstQuery *" is okay but vice versa isn't.

Does C++ have a better story in this area?  I haven't touched it
in so long that I don't remember.

            regards, tom lane



Re: Violation of principle that plan trees are read-only

From
Jose Luis Tallon
Date:
On 19/5/25 16:45, Tom Lane wrote:
> [snip]
> For one thing, I'm not sure how to teach the compiler that casting
> "Query *" to "ConstQuery *" is okay but vice versa isn't.
>
> Does C++ have a better story in this area?

Hi,

     A C++ compiler *does* enforce "const correctness" (for examples 
see, for example, Meyer's fantastic "Efficient C++" series).

It seems that Postgres is approaching the class of complexity/maturity 
that (modern) C++ was designed to solve. It should be unmatched in that 
area. I believe that, with recent platform deprecations, the vast 
majority of systems where one can compile a modern Postgres should 
already have a decent C++ compiler available (g++ & clang probably cover 
 >95%)

As opposed to say, rewriting in Rust ---where the compilar also does a 
very substantial checking of semantics at compile time---, introducing 
some C++ in Postgres could be as simple as throwing some "#ifdef 
__cplusplus__  extern "C" { #endif" in the headers and use the required 
subset in certain modules *only*.

     (this is how, for instance, Bacula did it ~two decades ago)

The planner and parts of the bufmgr / arenas (palloc et al.) seem to me 
as the most obvious candidates to try. I'm not sure whether it'd 
introduce any unintended regressions, though.

> I haven't touched it
> in so long that I don't remember.

C++ 17 would be the standard to tackle, IMHO.


HTH,

     J.L.

-- 
Parkinson's Law: Work expands to fill the time alloted to it.




Re: Violation of principle that plan trees are read-only

From
Andres Freund
Date:
Hi,

On 2025-05-18 19:31:33 -0400, Tom Lane wrote:
> While chasing down Valgrind leakage reports, I was disturbed
> to realize that some of them arise from a case where the
> executor scribbles on the plan tree it's given, which it is
> absolutely not supposed to do:
> 
>         /*
>          * Initialize result tuple slot and assign its rowtype using the first
>          * RETURNING list.  We assume the rest will look the same.
>          */
>         mtstate->ps.plan->targetlist = (List *) linitial(returningLists);
> 
> A bit of git archaeology fingers Andres' commit 4717fdb14, which we
> can't easily revert since he later got rid of ExecAssignResultType
> altogether.  But I think we need to do something about it --- it's
> purest luck that this doesn't cause serious problems in some cases.

I have no idea what I was smoking at that time, this clearly is wrong.

I think the reason it doesn't cause problems is that we're just using the
first child's targetlist every time and we're just assigning the same value
back every time.


What's even more absurd is: Why do we even need to assign the result type at
all? Before & after 4717fdb14. The planner ought to have already figured this
out, no?

It seems that if not, we'd have a problem anyway, who says the "calling" nodes
(say a wCTE) can cope with whatever output we come up with?

Except of course, there is exactly one case in our tests where the tupledescs
aren't equal :(


I've not dug fully into this, but I thought I should send this email to
confirm that I'm looking into the issue.

Greetings,

Andres Freund



Re: Violation of principle that plan trees are read-only

From
Tom Lane
Date:
I wrote:
> I think we can just delete this assignment (and fix these comments).

As attached.  I'm tempted to back-patch this: the plan tree damage
seems harmless at present, but maybe it'd become less harmless with
future fixes.

            regards, tom lane

diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 46d533b7288..71bc174cfc9 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -4830,12 +4830,11 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
         ExprContext *econtext;

         /*
-         * Initialize result tuple slot and assign its rowtype using the first
-         * RETURNING list.  We assume the rest will look the same.
+         * Initialize result tuple slot and assign its rowtype using the plan
+         * node's declared targetlist, which the planner set up to be the same
+         * as the first RETURNING list.  We assume the rest will produce
+         * compatible output.
          */
-        mtstate->ps.plan->targetlist = (List *) linitial(returningLists);
-
-        /* Set up a slot for the output of the RETURNING projection(s) */
         ExecInitResultTupleSlotTL(&mtstate->ps, &TTSOpsVirtual);
         slot = mtstate->ps.ps_ResultTupleSlot;

diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 150e9f060ee..8e1eb77dd49 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -1097,9 +1097,10 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)

                     /*
                      * Set up the visible plan targetlist as being the same as
-                     * the first RETURNING list. This is for the use of
-                     * EXPLAIN; the executor won't pay any attention to the
-                     * targetlist.  We postpone this step until here so that
+                     * the first RETURNING list.  This is mostly for the use
+                     * of EXPLAIN; the executor won't execute that targetlist,
+                     * although it does use it to prepare the node's result
+                     * tuple slot.  We postpone this step until here so that
                      * we don't have to do set_returning_clause_references()
                      * twice on identical targetlists.
                      */