Thread

Commits

Same data as JSON: GET /api/v1/messages/:b64id/commits the thread's linked commits as JSON, with link sources. API reference →
  1. Fix bogus ctid requirement for dummy-root partitioned targets

  1. BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    PG Bug reporting form <noreply@postgresql.org> — 2025-10-29T20:00:02Z

    The following bug has been logged on the website:
    
    Bug reference:      19099
    Logged by:          Alexander Lakhin
    Email address:      exclusion@gmail.com
    PostgreSQL version: 18.0
    Operating system:   Ubuntu 24.04
    Description:        
    
    The following script:
    CREATE EXTENSION file_fdw;
    CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw;
    CREATE TABLE pt (a int, b text) partition by list (a);
    CREATE FOREIGN TABLE p1 partition of pt for values in (1) SERVER file_server
    OPTIONS (format 'csv', filename '/tmp/1.csv');
    SET enable_partition_pruning = 'off';
    EXPLAIN DELETE FROM pt WHERE false;
    
    raises:
    ERROR:  XX000: could not find junk ctid column
    LOCATION:  ExecInitModifyTable, nodeModifyTable.c:4867
    (Discovered with SQLsmith.)
    
    Reproduced starting from 86dc9005.
    
    On 86dc9005~1 or with enable_partition_pruning = 'on', EXPLAIN outputs the
    query plan and "DELETE FROM pt WHERE false;" completes with no error.
    
    
  2. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    jian he <jian.universality@gmail.com> — 2025-10-30T04:07:03Z

    On Thu, Oct 30, 2025 at 9:02 AM PG Bug reporting form
    <noreply@postgresql.org> wrote:
    >
    > The following bug has been logged on the website:
    >
    > Bug reference:      19099
    > Logged by:          Alexander Lakhin
    > Email address:      exclusion@gmail.com
    > PostgreSQL version: 18.0
    > Operating system:   Ubuntu 24.04
    > Description:
    >
    > The following script:
    > CREATE EXTENSION file_fdw;
    > CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw;
    > CREATE TABLE pt (a int, b text) partition by list (a);
    > CREATE FOREIGN TABLE p1 partition of pt for values in (1) SERVER file_server
    > OPTIONS (format 'csv', filename '/tmp/1.csv');
    > SET enable_partition_pruning = 'off';
    > EXPLAIN DELETE FROM pt WHERE false;
    >
    > raises:
    > ERROR:  XX000: could not find junk ctid column
    > LOCATION:  ExecInitModifyTable, nodeModifyTable.c:4867
    > (Discovered with SQLsmith.)
    >
    > Reproduced starting from 86dc9005.
    >
    > On 86dc9005~1 or with enable_partition_pruning = 'on', EXPLAIN outputs the
    > query plan and "DELETE FROM pt WHERE false;" completes with no error.
    >
    
    we can add a postgresAddForeignUpdateTargets(postgres_fdw) equivalent
    function for file_fdw even though we do not support UPDATE/DELETE in file_fdw.
    
  3. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tender Wang <tndrwang@gmail.com> — 2025-10-30T04:41:01Z

    jian he <jian.universality@gmail.com> 于2025年10月30日周四 12:07写道:
    
    > On Thu, Oct 30, 2025 at 9:02 AM PG Bug reporting form
    > <noreply@postgresql.org> wrote:
    > >
    > > The following bug has been logged on the website:
    > >
    > > Bug reference:      19099
    > > Logged by:          Alexander Lakhin
    > > Email address:      exclusion@gmail.com
    > > PostgreSQL version: 18.0
    > > Operating system:   Ubuntu 24.04
    > > Description:
    > >
    > > The following script:
    > > CREATE EXTENSION file_fdw;
    > > CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw;
    > > CREATE TABLE pt (a int, b text) partition by list (a);
    > > CREATE FOREIGN TABLE p1 partition of pt for values in (1) SERVER
    > file_server
    > > OPTIONS (format 'csv', filename '/tmp/1.csv');
    > > SET enable_partition_pruning = 'off';
    > > EXPLAIN DELETE FROM pt WHERE false;
    > >
    > > raises:
    > > ERROR:  XX000: could not find junk ctid column
    > > LOCATION:  ExecInitModifyTable, nodeModifyTable.c:4867
    > > (Discovered with SQLsmith.)
    > >
    > > Reproduced starting from 86dc9005.
    > >
    > > On 86dc9005~1 or with enable_partition_pruning = 'on', EXPLAIN outputs
    > the
    > > query plan and "DELETE FROM pt WHERE false;" completes with no error.
    > >
    >
    > we can add a postgresAddForeignUpdateTargets(postgres_fdw) equivalent
    > function for file_fdw even though we do not support UPDATE/DELETE in
    > file_fdw.
    >
    
    After applying your patch,  I got a different output if I enable verbose in
    EXPLAIN:
    postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
                          QUERY PLAN
    -------------------------------------------------------
     Delete on public.pt  (cost=0.00..0.00 rows=0 width=0)
       ->  Result  (cost=0.00..0.00 rows=0 width=0)
             Output: ctid
             Replaces: Scan on pt
             One-Time Filter: false
    (5 rows)
    
    postgres=# set enable_partition_pruning = 'off';
    SET
    postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
                          QUERY PLAN
    -------------------------------------------------------
     Delete on public.pt  (cost=0.00..0.00 rows=0 width=0)
       ->  Result  (cost=0.00..0.00 rows=0 width=0)
             Output: NULL::oid, NULL::tid
             Replaces: Scan on pt
             One-Time Filter: false
    (5 rows)
    
     Output: ctid (enable_partition_pruning = on)
    vs
    Output: NULL::oid, NULL::tid(enable_partition_pruning = off)
    
    I try add childrte->relkind != RELKIND_PARTITIONED_TABLE
    && childrte->relkind != RELKIND_FOREIGN_TABLE)
    to avoid adding "tableoid" for foreign table
    in expand_single_inheritance_child().
    It works, but the file_fdw regression test failed.
    
    I added Tom and Amit to the cc list.
    Any thoughts?
    -- 
    Thanks,
    Tender Wang
    
  4. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T04:52:33Z

    On Thu, 30 Oct 2025 at 09:41, Tender Wang <tndrwang@gmail.com> wrote:
    >
    >
    >
    > jian he <jian.universality@gmail.com> 于2025年10月30日周四 12:07写道:
    >>
    >> On Thu, Oct 30, 2025 at 9:02 AM PG Bug reporting form
    >> <noreply@postgresql.org> wrote:
    >> >
    >> > The following bug has been logged on the website:
    >> >
    >> > Bug reference:      19099
    >> > Logged by:          Alexander Lakhin
    >> > Email address:      exclusion@gmail.com
    >> > PostgreSQL version: 18.0
    >> > Operating system:   Ubuntu 24.04
    >> > Description:
    >> >
    >> > The following script:
    >> > CREATE EXTENSION file_fdw;
    >> > CREATE SERVER file_server FOREIGN DATA WRAPPER file_fdw;
    >> > CREATE TABLE pt (a int, b text) partition by list (a);
    >> > CREATE FOREIGN TABLE p1 partition of pt for values in (1) SERVER file_server
    >> > OPTIONS (format 'csv', filename '/tmp/1.csv');
    >> > SET enable_partition_pruning = 'off';
    >> > EXPLAIN DELETE FROM pt WHERE false;
    >> >
    >> > raises:
    >> > ERROR:  XX000: could not find junk ctid column
    >> > LOCATION:  ExecInitModifyTable, nodeModifyTable.c:4867
    >> > (Discovered with SQLsmith.)
    >> >
    >> > Reproduced starting from 86dc9005.
    >> >
    >> > On 86dc9005~1 or with enable_partition_pruning = 'on', EXPLAIN outputs the
    >> > query plan and "DELETE FROM pt WHERE false;" completes with no error.
    >> >
    >>
    >> we can add a postgresAddForeignUpdateTargets(postgres_fdw) equivalent
    >> function for file_fdw even though we do not support UPDATE/DELETE in file_fdw.
    >
    >
    > After applying your patch,  I got a different output if I enable verbose in EXPLAIN:
    > postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
    >                       QUERY PLAN
    > -------------------------------------------------------
    >  Delete on public.pt  (cost=0.00..0.00 rows=0 width=0)
    >    ->  Result  (cost=0.00..0.00 rows=0 width=0)
    >          Output: ctid
    >          Replaces: Scan on pt
    >          One-Time Filter: false
    > (5 rows)
    >
    > postgres=# set enable_partition_pruning = 'off';
    > SET
    > postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
    >                       QUERY PLAN
    > -------------------------------------------------------
    >  Delete on public.pt  (cost=0.00..0.00 rows=0 width=0)
    >    ->  Result  (cost=0.00..0.00 rows=0 width=0)
    >          Output: NULL::oid, NULL::tid
    >          Replaces: Scan on pt
    >          One-Time Filter: false
    > (5 rows)
    >
    >  Output: ctid (enable_partition_pruning = on)
    > vs
    > Output: NULL::oid, NULL::tid(enable_partition_pruning = off)
    >
    > I try add childrte->relkind != RELKIND_PARTITIONED_TABLE && childrte->relkind != RELKIND_FOREIGN_TABLE)
    > to avoid adding "tableoid" for foreign table in expand_single_inheritance_child().
    > It works, but the file_fdw regression test failed.
    >
    > I added Tom and Amit to the cc list.
    > Any thoughts?
    > --
    > Thanks,
    > Tender Wang
    
    
    Hi!
    Jian's fix WFM, I confirm 'EXPLAIN DELETE FROM pt WHERE false' now
    works. Should we add this test case to the regression suite of
    file_fdw?
    
    But I also wonder if Jian's fix fixed the right thing. Should we
    instead fail in the planning phase with a more user-friendly error
    message? This will be a regression though, because 'DELETE FROM
    file_fdw_table WHERE false' will no longer work...
    
    As for EXPLAIN VERBOSE output, are they both confusing? Both for
    enable_partition_pruning=on and enable_partition_pruning=off? I mean,
    file_fdw does not have semantics of neither ctid nor tid?
    
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  5. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-30T05:08:23Z

    Tender Wang <tndrwang@gmail.com> writes:
    > I added Tom and Amit to the cc list.
    > Any thoughts?
    
    I'm having a hard time getting super excited about this.  file_fdw
    does not support DELETE -- it provides no ExecForeignDelete method --
    which is why you get this:
    
    regression=# DELETE FROM pt;
    ERROR:  cannot delete from foreign table "p1"
    
    It's surely pretty accidental (and arguably not desirable)
    if "DELETE FROM pt WHERE false" doesn't fail the same way.
    
    Now, I agree that it's not great if you instead get an
    internal error like "could not find junk ctid column".
    But that smells to me like error checks being applied in
    the wrong order rather than something fundamentally wrong.
    
    I didn't look at the proposed patch yet.
    
    			regards, tom lane
    
    
    
    
  6. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T05:31:38Z

    On Thu, 30 Oct 2025 at 10:08, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    
    >
    > It's surely pretty accidental (and arguably not desirable)
    > if "DELETE FROM pt WHERE false" doesn't fail the same way.
    >
    > Now, I agree that it's not great if you instead get an
    > internal error like "could not find junk ctid column".
    > But that smells to me like error checks being applied in
    > the wrong order rather than something fundamentally wrong.
    >
    > I didn't look at the proposed patch yet.
    >
    >                         regards, tom lane
    
    I wrote:
    
    > But I also wonder if Jian's fix fixed the right thing. Should we
    > instead fail in the planning phase with a more user-friendly error
    > message? This will be a regression though, because 'DELETE FROM
    > file_fdw_table WHERE false' will no longer work...
    
    On the second thought, I doubt anyone will get unhappy with 'DELETE
    FROM file_fdw_table WHERE false' stop working
    
    Alexander wrote:
    
    > On 86dc9005~1 or with enable_partition_pruning = 'on', EXPLAIN outputs the
    > query plan and "DELETE FROM pt WHERE false;" completes with no error.
    
    So, behaviour was wrong both before and after  86dc9005, just in different ways.
    
    On head, we get an error about junk columns, because without partition
    pruning, we derive the result relation as 'pt', not its partition
    'p1', which is correct I believe.  But with 'p1' as result relation,
    we (correctly) error out in ExecInitModifyTable while with 'pt' we
    don't.
    
    So, error checks are applied, order is not wrong, but rather checks
    are not full enough?  I mean, we I believe we need to execute
    CheckValidResultRel against all partitions in ExecInitModifyTable, at
    least when no partition pruning has been performed
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  7. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T06:44:07Z

    On Thu, 30 Oct 2025 at 10:31, I wrote:
    >
    >  I mean, we I believe we need to execute
    > CheckValidResultRel against all partitions in ExecInitModifyTable, at
    > least when no partition pruning has been performed
    >
    
    So, the problem is that we managed to exclude all child relations, and
    only have a single (dummy) root relation as a result of the
    modifyTable plan. Maybe we should populate its target list with
    pseudo-junk columns in create_modifytable_plan ?
    
    For instance, they query does not error-out if we have at least one
    another non-file-fdw partition:
    
    create table p2 partition of pt for values in ( 2) ;
    
    this is because we have this in create_modifytable_plan
    
    ```
    /* Transfer resname/resjunk labeling, too, to keep executor happy */
    apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
    ```
    
    and we successfully found a junk column in the p2 partition.
    
    The problem is, it works iff root->processed_tlist has at least one
    relation which can give us junk columns. Should we add handling for
    corner case here?
    Another option is to remove this 'Transfer resname/resjunk labeling'
    completely and rework planner-executer contracts somehow.
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  8. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-10-30T08:29:37Z

    Hi,
    
    On Thu, Oct 30, 2025 at 3:44 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > On Thu, 30 Oct 2025 at 10:31, I wrote:
    > >
    > >  I mean, we I believe we need to execute
    > > CheckValidResultRel against all partitions in ExecInitModifyTable, at
    > > least when no partition pruning has been performed
    > >
    >
    > So, the problem is that we managed to exclude all child relations, and
    > only have a single (dummy) root relation as a result of the
    > modifyTable plan. Maybe we should populate its target list with
    > pseudo-junk columns in create_modifytable_plan ?
    >
    > For instance, they query does not error-out if we have at least one
    > another non-file-fdw partition:
    >
    > create table p2 partition of pt for values in ( 2) ;
    >
    > this is because we have this in create_modifytable_plan
    >
    > ```
    > /* Transfer resname/resjunk labeling, too, to keep executor happy */
    > apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
    > ```
    >
    > and we successfully found a junk column in the p2 partition.
    >
    > The problem is, it works iff root->processed_tlist has at least one
    > relation which can give us junk columns. Should we add handling for
    > corner case here?
    > Another option is to remove this 'Transfer resname/resjunk labeling'
    > completely and rework planner-executer contracts somehow.
    
    I am not really sure if we should play with the planner code.
    
    I suspect the real issue is that we’re assuming partitioned tables
    always need a ctid, which wasn’t true before MERGE started using the
    root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    been requiring a ctid even for partitioned tables where that was never
    necessary. We can fix this by only requiring the junk ctid when we
    actually operate through the root partitioned table, that is, for
    MERGE.  Like the attached.
    
    -- 
    Thanks, Amit Langote
    
  9. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T08:55:18Z

    On Thu, 30 Oct 2025 at 13:29, Amit Langote <amitlangote09@gmail.com> wrote:
    >
    > Hi,
    >
    > On Thu, Oct 30, 2025 at 3:44 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > > On Thu, 30 Oct 2025 at 10:31, I wrote:
    > > >
    > > >  I mean, we I believe we need to execute
    > > > CheckValidResultRel against all partitions in ExecInitModifyTable, at
    > > > least when no partition pruning has been performed
    > > >
    > >
    > > So, the problem is that we managed to exclude all child relations, and
    > > only have a single (dummy) root relation as a result of the
    > > modifyTable plan. Maybe we should populate its target list with
    > > pseudo-junk columns in create_modifytable_plan ?
    > >
    > > For instance, they query does not error-out if we have at least one
    > > another non-file-fdw partition:
    > >
    > > create table p2 partition of pt for values in ( 2) ;
    > >
    > > this is because we have this in create_modifytable_plan
    > >
    > > ```
    > > /* Transfer resname/resjunk labeling, too, to keep executor happy */
    > > apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
    > > ```
    > >
    > > and we successfully found a junk column in the p2 partition.
    > >
    > > The problem is, it works iff root->processed_tlist has at least one
    > > relation which can give us junk columns. Should we add handling for
    > > corner case here?
    > > Another option is to remove this 'Transfer resname/resjunk labeling'
    > > completely and rework planner-executer contracts somehow.
    >
    > I am not really sure if we should play with the planner code.
    >
    > I suspect the real issue is that we’re assuming partitioned tables
    > always need a ctid, which wasn’t true before MERGE started using the
    > root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    > been requiring a ctid even for partitioned tables where that was never
    > necessary. We can fix this by only requiring the junk ctid when we
    > actually operate through the root partitioned table, that is, for
    > MERGE.  Like the attached.
    >
    > --
    > Thanks, Amit Langote
    
    Hi! Thanks for the patch. I can see your points, however I am unsure
    if this is the most right thing to do.
    As per ab5fcf2b04f9 commit message and
    src/backend/optimizer/plan/planner.c comments, I am under impression
    that the postgres-way of fixing that would allow for
    ExecInitModifyTable to operate with a NULL result relation list.
    And, in any case, I am still unsure if we should allow the 'DELETE'
    statement from Alexander's repro to successfully execute, which yout
    patch still does
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  10. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-10-30T09:17:43Z

    On Thu, Oct 30, 2025 at 5:55 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > On Thu, 30 Oct 2025 at 13:29, Amit Langote <amitlangote09@gmail.com> wrote:
    > >
    > > Hi,
    > >
    > > On Thu, Oct 30, 2025 at 3:44 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > > > On Thu, 30 Oct 2025 at 10:31, I wrote:
    > > > >
    > > > >  I mean, we I believe we need to execute
    > > > > CheckValidResultRel against all partitions in ExecInitModifyTable, at
    > > > > least when no partition pruning has been performed
    > > > >
    > > >
    > > > So, the problem is that we managed to exclude all child relations, and
    > > > only have a single (dummy) root relation as a result of the
    > > > modifyTable plan. Maybe we should populate its target list with
    > > > pseudo-junk columns in create_modifytable_plan ?
    > > >
    > > > For instance, they query does not error-out if we have at least one
    > > > another non-file-fdw partition:
    > > >
    > > > create table p2 partition of pt for values in ( 2) ;
    > > >
    > > > this is because we have this in create_modifytable_plan
    > > >
    > > > ```
    > > > /* Transfer resname/resjunk labeling, too, to keep executor happy */
    > > > apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
    > > > ```
    > > >
    > > > and we successfully found a junk column in the p2 partition.
    > > >
    > > > The problem is, it works iff root->processed_tlist has at least one
    > > > relation which can give us junk columns. Should we add handling for
    > > > corner case here?
    > > > Another option is to remove this 'Transfer resname/resjunk labeling'
    > > > completely and rework planner-executer contracts somehow.
    > >
    > > I am not really sure if we should play with the planner code.
    > >
    > > I suspect the real issue is that we’re assuming partitioned tables
    > > always need a ctid, which wasn’t true before MERGE started using the
    > > root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    > > been requiring a ctid even for partitioned tables where that was never
    > > necessary. We can fix this by only requiring the junk ctid when we
    > > actually operate through the root partitioned table, that is, for
    > > MERGE.  Like the attached.
    > >
    > > --
    > > Thanks, Amit Langote
    >
    > Hi! Thanks for the patch. I can see your points, however I am unsure
    > if this is the most right thing to do.
    > As per ab5fcf2b04f9 commit message and
    > src/backend/optimizer/plan/planner.c comments, I am under impression
    > that the postgres-way of fixing that would allow for
    > ExecInitModifyTable to operate with a NULL result relation list.
    
    Isn't that what happens with my patch?
    
    > And, in any case, I am still unsure if we should allow the 'DELETE'
    > statement from Alexander's repro to successfully execute, which yout
    > patch still does
    
    What behavior do you propose in that case? The WHERE false part makes
    the plan a dummy ModifyTable on the root partitioned table pt (per
    ab5fcf2b0 I guess), and there’s nothing left in the plan that can be
    flagged at execution time; the error Alexander reported is a bug we're
    trying to fix.  Are you suggesting instead that the attempt to plan
    DELETE on the file_fdw partition -- or any foreign table that doesn’t
    support DELETE -- should be prevented?
    
    -- 
    Thanks, Amit Langote
    
    
    
    
  11. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T09:53:23Z

    On Thu, 30 Oct 2025, 14:18 Amit Langote, <amitlangote09@gmail.com> wrote:
    
    > On Thu, Oct 30, 2025 at 5:55 PM Kirill Reshke <reshkekirill@gmail.com>
    > wrote:
    > > On Thu, 30 Oct 2025 at 13:29, Amit Langote <amitlangote09@gmail.com>
    > wrote:
    > > >
    > > > Hi,
    > > >
    > > > On Thu, Oct 30, 2025 at 3:44 PM Kirill Reshke <reshkekirill@gmail.com>
    > wrote:
    > > > > On Thu, 30 Oct 2025 at 10:31, I wrote:
    > > > > >
    > > > > >  I mean, we I believe we need to execute
    > > > > > CheckValidResultRel against all partitions in ExecInitModifyTable,
    > at
    > > > > > least when no partition pruning has been performed
    > > > > >
    > > > >
    > > > > So, the problem is that we managed to exclude all child relations,
    > and
    > > > > only have a single (dummy) root relation as a result of the
    > > > > modifyTable plan. Maybe we should populate its target list with
    > > > > pseudo-junk columns in create_modifytable_plan ?
    > > > >
    > > > > For instance, they query does not error-out if we have at least one
    > > > > another non-file-fdw partition:
    > > > >
    > > > > create table p2 partition of pt for values in ( 2) ;
    > > > >
    > > > > this is because we have this in create_modifytable_plan
    > > > >
    > > > > ```
    > > > > /* Transfer resname/resjunk labeling, too, to keep executor happy */
    > > > > apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
    > > > > ```
    > > > >
    > > > > and we successfully found a junk column in the p2 partition.
    > > > >
    > > > > The problem is, it works iff root->processed_tlist has at least one
    > > > > relation which can give us junk columns. Should we add handling for
    > > > > corner case here?
    > > > > Another option is to remove this 'Transfer resname/resjunk labeling'
    > > > > completely and rework planner-executer contracts somehow.
    > > >
    > > > I am not really sure if we should play with the planner code.
    > > >
    > > > I suspect the real issue is that we’re assuming partitioned tables
    > > > always need a ctid, which wasn’t true before MERGE started using the
    > > > root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    > > > been requiring a ctid even for partitioned tables where that was never
    > > > necessary. We can fix this by only requiring the junk ctid when we
    > > > actually operate through the root partitioned table, that is, for
    > > > MERGE.  Like the attached.
    > > >
    > > > --
    > > > Thanks, Amit Langote
    > >
    > > Hi! Thanks for the patch. I can see your points, however I am unsure
    > > if this is the most right thing to do.
    > > As per ab5fcf2b04f9 commit message and
    > > src/backend/optimizer/plan/planner.c comments, I am under impression
    > > that the postgres-way of fixing that would allow for
    > > ExecInitModifyTable to operate with a NULL result relation list.
    >
    > Isn't that what happens with my patch?
    >
    > > And, in any case, I am still unsure if we should allow the 'DELETE'
    > > statement from Alexander's repro to successfully execute, which yout
    > > patch still does
    >
    > What behavior do you propose in that case? The WHERE false part makes
    > the plan a dummy ModifyTable on the root partitioned table pt (per
    > ab5fcf2b0 I guess), and there’s nothing left in the plan that can be
    > flagged at execution time; the error Alexander reported is a bug we're
    > trying to fix.  Are you suggesting instead that the attempt to plan
    > DELETE on the file_fdw partition -- or any foreign table that doesn’t
    > support DELETE -- should be prevented?
    >
    > --
    > Thanks, Amit Langote
    >
    
    
    Okay, after putting more thought on it, I think your fix is OK. WFM, LGTM
    
    >
    
  12. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tender Wang <tndrwang@gmail.com> — 2025-10-30T11:08:26Z

    Amit Langote <amitlangote09@gmail.com> 于2025年10月30日周四 16:29写道:
    
    >
    >
    > I am not really sure if we should play with the planner code.
    >
    > I suspect the real issue is that we’re assuming partitioned tables
    > always need a ctid, which wasn’t true before MERGE started using the
    > root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    > been requiring a ctid even for partitioned tables where that was never
    > necessary. We can fix this by only requiring the junk ctid when we
    > actually operate through the root partitioned table, that is, for
    > MERGE.  Like the attached.
    >
    
    With your patch, this issue didn't happen again.
    But I still get a different output when I enable verbose in EXPLAIN,
    
    Output: ctid (enable_partition_pruning = on)
    vs
    Output: NULL::oid(enable_partition_pruning = off)
    
    From the user's perspective, it's a bit confusing.
    I agree more with Tom’s opinion — we should throw an error like "cannot
    delete from foreign table p1"
    But the plan only had a dummy root relation; CheckValidResultRel() doesn't
    work.
    Some other code place may need to do something.
    
    
    -- 
    Thanks,
    Tender Wang
    
  13. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-10-30T13:17:11Z

    On Thu, Oct 30, 2025 at 8:08 PM Tender Wang <tndrwang@gmail.com> wrote:
    > Amit Langote <amitlangote09@gmail.com> 于2025年10月30日周四 16:29写道:
    >> I am not really sure if we should play with the planner code.
    >>
    >> I suspect the real issue is that we’re assuming partitioned tables
    >> always need a ctid, which wasn’t true before MERGE started using the
    >> root ResultRelInfo. In fact, the old code already looked wrong -- it’s
    >> been requiring a ctid even for partitioned tables where that was never
    >> necessary. We can fix this by only requiring the junk ctid when we
    >> actually operate through the root partitioned table, that is, for
    >> MERGE.  Like the attached.
    >
    >
    > With your patch, this issue didn't happen again.
    > But I still get a different output when I enable verbose in EXPLAIN,
    >
    > Output: ctid (enable_partition_pruning = on)
    > vs
    > Output: NULL::oid(enable_partition_pruning = off)
    >
    > From the user's perspective, it's a bit confusing.
    
    Hmm, that's perhaps not ideal.  That's the row identity var "tableoid"
    added by expand_single_inheritance_child():
    
                /*
                 * If we have any child target relations, assume they all need to
                 * generate a junk "tableoid" column.  (If only one child survives
                 * pruning, we wouldn't really need this, but it's not worth
                 * thrashing about to avoid it.)
                 */
                rrvar = makeVar(childRTindex,
                                TableOidAttributeNumber,
                                OIDOID,
                                -1,
                                InvalidOid,
                                0);
                add_row_identity_var(root, rrvar, childRTindex, "tableoid");
    
    The WHERE false excludes the child that adds the above var, so you end
    up with a NULL in the targetlist because of this part of
    set_plan_refs():
    
                        /*
                         * The tlist of a childless Result could contain
                         * unresolved ROWID_VAR Vars, in case it's representing a
                         * target relation which is completely empty because of
                         * constraint exclusion.  Replace any such Vars by null
                         * constants, as though they'd been resolved for a leaf
                         * scan node that doesn't support them.  We could have
                         * fix_scan_expr do this, but since the case is only
                         * expected to occur here, it seems safer to special-case
                         * it here and keep the assertions that ROWID_VARs
                         * shouldn't be seen by fix_scan_expr.
                         *
                         * We also must handle the case where set operations have
                         * been short-circuited resulting in a dummy Result node.
                         * prepunion.c uses varno==0 for the set op targetlist.
                         * See generate_setop_tlist() and generate_setop_tlist().
                         * Here we rewrite these to use varno==1, which is the
                         * varno of the first set-op child.  Without this, EXPLAIN
                         * will have trouble displaying targetlists of dummy set
                         * operations.
                         */
                        foreach(l, splan->plan.targetlist)
                        {
                            TargetEntry *tle = (TargetEntry *) lfirst(l);
                            Var        *var = (Var *) tle->expr;
    
                            if (var && IsA(var, Var))
                            {
                                if (var->varno == ROWID_VAR)
                                    tle->expr = (Expr *) makeNullConst(var->vartype,
    
    var->vartypmod,
    
    var->varcollid);
    
    I’m not sure how hard we should try to avoid that kind of confusion in
    the EXPLAIN VERBOSE output.
    
    > I agree more with Tom’s opinion — we should throw an error like "cannot delete from foreign table p1"
    > But the plan only had a dummy root relation; CheckValidResultRel() doesn't work.
    > Some other code place may need to do something.
    
    Yeah, I’m also not sure there’s an obvious place where we could detect
    that earlier. Once pruning removes all real children, the planner ends
    up with just a dummy root, and by that point there’s no surviving
    foreign table ResultRelInfo to check or even any child relation data
    structure in the planner.
    
    --
    Thanks, Amit Langote
    
    
    
    
  14. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-10-30T13:31:09Z

    Hi!
    
    On Thu, 30 Oct 2025 at 16:08, Tender Wang <tndrwang@gmail.com> wrote:
    >
    > From the user's perspective, it's a bit confusing.
    > I agree more with Tom’s opinion — we should throw an error like "cannot delete from foreign table p1"
    > But the plan only had a dummy root relation; CheckValidResultRel() doesn't work.
    > Some other code place may need to do something.
    >
    
    
    Tom wrote:
    > It's surely pretty accidental (and arguably not desirable)
    > if "DELETE FROM pt WHERE false" doesn't fail the same way.
    
    I cannot prove to myself why failing here is actually desirable. Can
    you elaborate?
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  15. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tom Lane <tgl@sss.pgh.pa.us> — 2025-10-30T13:48:32Z

    Kirill Reshke <reshkekirill@gmail.com> writes:
    > Tom wrote:
    >> It's surely pretty accidental (and arguably not desirable)
    >> if "DELETE FROM pt WHERE false" doesn't fail the same way.
    
    > I cannot prove to myself why failing here is actually desirable. Can
    > you elaborate?
    
    If we throw that failure in some cases but not others, we're exposing
    implementation details.
    
    The definition could have been "throw 'cannot delete from foreign
    table' only if the query actually attempts to delete some specific
    row from some foreign table", but it is not implemented that way.
    Instead the error is thrown during query startup if the query has
    a foreign table as a potential delete target.  Thus, as things stand
    today, you might or might not get the error depending on whether
    the planner can prove that that partition won't be deleted from.
    This is not a great user experience, because we don't (and won't)
    make any hard promises about how smart the planner is.
    
    An analogy perhaps is that whether you get a "permission denied"
    error about some target table is not conditional on whether the
    query actually attempts to delete any rows from it.  We go out
    of our way to make sure that that happens when required by spec,
    even if the planner is able to prove that no delete will happen.
    
    None of this is meant to justify throwing an internal error here;
    that's clearly bad.  I'm just saying that there would be little
    wrong with fixing it by throwing "cannot delete" instead.  The user
    has no right to expect that that won't happen in a case like this.
    
    			regards, tom lane
    
    
    
    
  16. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-10-31T00:30:39Z

    On Thu, Oct 30, 2025 at 10:48 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > Kirill Reshke <reshkekirill@gmail.com> writes:
    > > Tom wrote:
    > >> It's surely pretty accidental (and arguably not desirable)
    > >> if "DELETE FROM pt WHERE false" doesn't fail the same way.
    >
    > > I cannot prove to myself why failing here is actually desirable. Can
    > > you elaborate?
    >
    > If we throw that failure in some cases but not others, we're exposing
    > implementation details.
    >
    > The definition could have been "throw 'cannot delete from foreign
    > table' only if the query actually attempts to delete some specific
    > row from some foreign table", but it is not implemented that way.
    > Instead the error is thrown during query startup if the query has
    > a foreign table as a potential delete target.  Thus, as things stand
    > today, you might or might not get the error depending on whether
    > the planner can prove that that partition won't be deleted from.
    > This is not a great user experience, because we don't (and won't)
    > make any hard promises about how smart the planner is.
    >
    > An analogy perhaps is that whether you get a "permission denied"
    > error about some target table is not conditional on whether the
    > query actually attempts to delete any rows from it.  We go out
    > of our way to make sure that that happens when required by spec,
    > even if the planner is able to prove that no delete will happen.
    >
    > None of this is meant to justify throwing an internal error here;
    > that's clearly bad.  I'm just saying that there would be little
    > wrong with fixing it by throwing "cannot delete" instead.  The user
    > has no right to expect that that won't happen in a case like this.
    
    We might be able to throw the "cannot delete from foreign table" like this:
    
    @@ -987,6 +987,16 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
    
             fdwroutine = GetFdwRoutineForRelation(target_relation, false);
    
    +        if (fdwroutine->ExecForeignDelete == NULL)
    +            ereport(ERROR,
    +                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +                     errmsg("cannot delete from foreign table \"%s\"",
    +                            RelationGetRelationName(target_relation))));
    +        if (fdwroutine->ExecForeignUpdate == NULL)
    +            ereport(ERROR,
    +                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
    +                     errmsg("cannot update foreign table \"%s\"",
    +                            RelationGetRelationName(target_relation))));
             if (fdwroutine->AddForeignUpdateTargets != NULL)
                 fdwroutine->AddForeignUpdateTargets(root, rtindex,
                                                     target_rte, target_relation);
    
    but I am not sure how consistent the following is after applying that:
    
    postgres=# set enable_partition_pruning to off;
    SET
    postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
    ERROR:  cannot delete from foreign table "p1"
    postgres=# set enable_partition_pruning to on;
    SET
    
    -- we don't even hit the foreign table in the planner
    postgres=# EXPLAIN verbose DELETE FROM pt WHERE false;
                          QUERY PLAN
    -------------------------------------------------------
     Delete on public.pt  (cost=0.00..0.00 rows=0 width=0)
       ->  Result  (cost=0.00..0.00 rows=0 width=0)
             Output: ctid
             Replaces: Scan on pt
             One-Time Filter: false
    (5 rows)
    
    -- 
    Thanks, Amit Langote
    
    
    
    
  17. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    David Rowley <dgrowleyml@gmail.com> — 2025-10-31T01:50:15Z

    On Fri, 31 Oct 2025 at 02:48, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > The definition could have been "throw 'cannot delete from foreign
    > table' only if the query actually attempts to delete some specific
    > row from some foreign table", but it is not implemented that way.
    > Instead the error is thrown during query startup if the query has
    > a foreign table as a potential delete target.  Thus, as things stand
    > today, you might or might not get the error depending on whether
    > the planner can prove that that partition won't be deleted from.
    > This is not a great user experience, because we don't (and won't)
    > make any hard promises about how smart the planner is.
    
    It's a good point, but I doubt we could change this fact as I expect
    there are people relying on pruned partitions being excluded from this
    check. It seems reasonable that someone might want to do something
    like archive ancient time-based partitioned table partitions into
    file_fdw stored on a compressed filesystem so that they can still at
    least query old data should they need to.  If we were to precheck that
    all partitions support an UPDATE/DELETE, then it could break workloads
    that do updates on recent data in heap-based partitions. Things would
    go bad for those people if they switched off partition pruning, but I
    doubt that would be the only reason as that would also add a huge
    amount of overhead to their SELECT statements.
    
    In any case, the planner is now very efficient at not loading any
    metadata for pruned partitions, so I don't see how we'd do this
    without adding possibly large overhead to the planner. I'd say we're
    well beyond the point of being able to change this now.
    
    David
    
    
    
    
  18. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-06T10:00:33Z

    On Fri, Oct 31, 2025 at 10:50 AM David Rowley <dgrowleyml@gmail.com> wrote:
    > On Fri, 31 Oct 2025 at 02:48, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > > The definition could have been "throw 'cannot delete from foreign
    > > table' only if the query actually attempts to delete some specific
    > > row from some foreign table", but it is not implemented that way.
    > > Instead the error is thrown during query startup if the query has
    > > a foreign table as a potential delete target.  Thus, as things stand
    > > today, you might or might not get the error depending on whether
    > > the planner can prove that that partition won't be deleted from.
    > > This is not a great user experience, because we don't (and won't)
    > > make any hard promises about how smart the planner is.
    >
    > It's a good point, but I doubt we could change this fact as I expect
    > there are people relying on pruned partitions being excluded from this
    > check. It seems reasonable that someone might want to do something
    > like archive ancient time-based partitioned table partitions into
    > file_fdw stored on a compressed filesystem so that they can still at
    > least query old data should they need to.  If we were to precheck that
    > all partitions support an UPDATE/DELETE, then it could break workloads
    > that do updates on recent data in heap-based partitions. Things would
    > go bad for those people if they switched off partition pruning, but I
    > doubt that would be the only reason as that would also add a huge
    > amount of overhead to their SELECT statements.
    >
    > In any case, the planner is now very efficient at not loading any
    > metadata for pruned partitions, so I don't see how we'd do this
    > without adding possibly large overhead to the planner. I'd say we're
    > well beyond the point of being able to change this now.
    
    I agree that we definitely shouldn’t load metadata for partitions that
    are excluded from the plan, especially not just to slightly improve
    user experience in this corner case.
    
    I looked at a few options, but none seem non-invasive enough for
    back-patching, apart from the first patch I posted. That one makes
    ExecInitModifyTable() not require a ctid to be present to set the root
    partitioned table’s ri_RowIdAttNo, except in the case of MERGE, which
    seems to need it. The corner case that triggers the internal error for
    UPDATE/DELETE doesn’t occur for MERGE now and likely won’t when
    foreign tables eventually gain MERGE support; don't mark my words
    though ;-).
    
    Among those options, I considered the following block, which adds a
    ctid for the partitioned root table when it’s the only target in the
    query after partition pruning removes all child tables due to the
    WHERE false condition in the problematic case:
    
        /*
         * Ordinarily, we expect that leaf result relation(s) will have added some
         * ROWID_VAR Vars to the query.  However, it's possible that constraint
         * exclusion suppressed every leaf relation.  The executor will get upset
         * if the plan has no row identity columns at all, even though it will
         * certainly process no rows.  Handle this edge case by re-opening the top
         * result relation and adding the row identity columns it would have used,
         * as preprocess_targetlist() would have done if it weren't marked "inh".
         * Then re-run build_base_rel_tlists() to ensure that the added columns
         * get propagated to the relation's reltarget.  (This is a bit ugly, but
         * it seems better to confine the ugliness and extra cycles to this
         * unusual corner case.)
         */
        if (root->row_identity_vars == NIL)
        {
            Relation    target_relation;
    
            target_relation = table_open(target_rte->relid, NoLock);
            add_row_identity_columns(root, result_relation,
                                     target_rte, target_relation);
            table_close(target_relation, NoLock);
            build_base_rel_tlists(root, root->processed_tlist);
            /* There are no ROWID_VAR Vars in this case, so we're done. */
            return;
        }
    
    If enable_partition_pruning is off, root->row_identity_vars already
    contains a RowIdentityVarInfo entry for the tableoid Var that was
    added while processing the foreign-table child partition. Because of
    that, the if (root->row_identity_vars == NIL) block doesn’t run in
    this case, so it won’t add any row identity columns such as ctid for
    the partitioned root table.
    
    In theory, we could prevent the planner from adding tableoid in the
    first place when the child table doesn’t support any row identity
    column -- or worse, doesn’t support the UPDATE/DELETE/MERGE command at
    all -- but doing so would require changing the order in which tableoid
    appears in root->processed_tlist. That would be too invasive for a
    back-patch.
    
    So for back branches, I’d propose sticking with the smaller
    executor-side fix and perhaps revisiting the planner behavior
    separately if we ever want to refine handling of pruned partitions or
    dummy roots. I understand, as was reported upthread, that the EXPLAIN
    VERBOSE output isn’t very consistent with that patch even though the
    internal error goes away.  Making sense of the output differences
    requires knowing that the targetlist population behavior differs
    depending on whether enable_partition_pruning is on or off as I
    described above.
    
    --
    Thanks, Amit Langote
    
    
    
    
  19. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tender Wang <tndrwang@gmail.com> — 2025-11-07T01:01:45Z

    Amit Langote <amitlangote09@gmail.com> 于2025年11月6日周四 18:00写道:
    
    > On Fri, Oct 31, 2025 at 10:50 AM David Rowley <dgrowleyml@gmail.com>
    > wrote:
    > > On Fri, 31 Oct 2025 at 02:48, Tom Lane <tgl@sss.pgh.pa.us> wrote:
    > > > The definition could have been "throw 'cannot delete from foreign
    > > > table' only if the query actually attempts to delete some specific
    > > > row from some foreign table", but it is not implemented that way.
    > > > Instead the error is thrown during query startup if the query has
    > > > a foreign table as a potential delete target.  Thus, as things stand
    > > > today, you might or might not get the error depending on whether
    > > > the planner can prove that that partition won't be deleted from.
    > > > This is not a great user experience, because we don't (and won't)
    > > > make any hard promises about how smart the planner is.
    > >
    > > It's a good point, but I doubt we could change this fact as I expect
    > > there are people relying on pruned partitions being excluded from this
    > > check. It seems reasonable that someone might want to do something
    > > like archive ancient time-based partitioned table partitions into
    > > file_fdw stored on a compressed filesystem so that they can still at
    > > least query old data should they need to.  If we were to precheck that
    > > all partitions support an UPDATE/DELETE, then it could break workloads
    > > that do updates on recent data in heap-based partitions. Things would
    > > go bad for those people if they switched off partition pruning, but I
    > > doubt that would be the only reason as that would also add a huge
    > > amount of overhead to their SELECT statements.
    > >
    > > In any case, the planner is now very efficient at not loading any
    > > metadata for pruned partitions, so I don't see how we'd do this
    > > without adding possibly large overhead to the planner. I'd say we're
    > > well beyond the point of being able to change this now.
    >
    > I agree that we definitely shouldn’t load metadata for partitions that
    > are excluded from the plan, especially not just to slightly improve
    > user experience in this corner case.
    >
    > I looked at a few options, but none seem non-invasive enough for
    > back-patching, apart from the first patch I posted. That one makes
    > ExecInitModifyTable() not require a ctid to be present to set the root
    > partitioned table’s ri_RowIdAttNo, except in the case of MERGE, which
    > seems to need it. The corner case that triggers the internal error for
    > UPDATE/DELETE doesn’t occur for MERGE now and likely won’t when
    > foreign tables eventually gain MERGE support; don't mark my words
    > though ;-).
    >
    > Among those options, I considered the following block, which adds a
    > ctid for the partitioned root table when it’s the only target in the
    > query after partition pruning removes all child tables due to the
    > WHERE false condition in the problematic case:
    >
    >     /*
    >      * Ordinarily, we expect that leaf result relation(s) will have added
    > some
    >      * ROWID_VAR Vars to the query.  However, it's possible that constraint
    >      * exclusion suppressed every leaf relation.  The executor will get
    > upset
    >      * if the plan has no row identity columns at all, even though it will
    >      * certainly process no rows.  Handle this edge case by re-opening the
    > top
    >      * result relation and adding the row identity columns it would have
    > used,
    >      * as preprocess_targetlist() would have done if it weren't marked
    > "inh".
    >      * Then re-run build_base_rel_tlists() to ensure that the added columns
    >      * get propagated to the relation's reltarget.  (This is a bit ugly,
    > but
    >      * it seems better to confine the ugliness and extra cycles to this
    >      * unusual corner case.)
    >      */
    >     if (root->row_identity_vars == NIL)
    >     {
    >         Relation    target_relation;
    >
    >         target_relation = table_open(target_rte->relid, NoLock);
    >         add_row_identity_columns(root, result_relation,
    >                                  target_rte, target_relation);
    >         table_close(target_relation, NoLock);
    >         build_base_rel_tlists(root, root->processed_tlist);
    >         /* There are no ROWID_VAR Vars in this case, so we're done. */
    >         return;
    >     }
    >
    > If enable_partition_pruning is off, root->row_identity_vars already
    > contains a RowIdentityVarInfo entry for the tableoid Var that was
    > added while processing the foreign-table child partition. Because of
    > that, the if (root->row_identity_vars == NIL) block doesn’t run in
    > this case, so it won’t add any row identity columns such as ctid for
    > the partitioned root table.
    >
    > In theory, we could prevent the planner from adding tableoid in the
    > first place when the child table doesn’t support any row identity
    > column -- or worse, doesn’t support the UPDATE/DELETE/MERGE command at
    > all -- but doing so would require changing the order in which tableoid
    > appears in root->processed_tlist. That would be too invasive for a
    > back-patch.
    
    
    Yeah, it seems to need more work if we prevent the planner from adding
    tableoid
    in the first place.
    
    
    
    
    > So for back branches, I’d propose sticking with the smaller
    > executor-side fix and perhaps revisiting the planner behavior
    > separately if we ever want to refine handling of pruned partitions or
    > dummy roots. I understand, as was reported upthread, that the EXPLAIN
    > VERBOSE output isn’t very consistent with that patch even though the
    > internal error goes away.  Making sense of the output differences
    > requires knowing that the targetlist population behavior differs
    > depending on whether enable_partition_pruning is on or off as I
    > described above.
    >
    
    The executor-side fix works for me and the test case should be added to
    your patch.
    Should we add some comments to explain the output difference in EXPLAIN
    VERBOSE
    if enable_partition_pruning is set to a different value?
    
    
    -- 
    Thanks,
    Tender Wang
    
  20. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-07T06:02:04Z

    On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > I looked at a few options, but none seem non-invasive enough for
    > back-patching, apart from the first patch I posted. That one makes
    > ExecInitModifyTable() not require a ctid to be present to set the root
    > partitioned table’s ri_RowIdAttNo, except in the case of MERGE, which
    > seems to need it. The corner case that triggers the internal error for
    > UPDATE/DELETE doesn’t occur for MERGE now and likely won’t when
    > foreign tables eventually gain MERGE support; don't mark my words
    > though ;-).
    
    Well, OK, I just had not tried hard enough to see that the same error
    happens for MERGE too.
    
    With my patch applied:
    EXPLAIN VERBOSE MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a,
    b) ON false WHEN MATCHED THEN UPDATE SET b = s.b;
    ERROR:  could not find junk ctid column
    
    I have another idea: we can simply recognize the corner condition that
    throws this error in ExecInitModifyTable() by checking if
    ModifyTable.resultRelations contains only the root partitioned table.
    That can only happen for UPDATE, DELETE, or MERGE when all child
    relations were excluded.
    
    Patch doing that attached.  Added test cases to file_fdw's suite.
    
    -- 
    Thanks, Amit Langote
    
  21. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-07T06:03:50Z

    Hi,
    
    On Fri, Nov 7, 2025 at 10:01 AM Tender Wang <tndrwang@gmail.com> wrote:
    > Amit Langote <amitlangote09@gmail.com> 于2025年11月6日周四 18:00写道:
    >> So for back branches, I’d propose sticking with the smaller
    >> executor-side fix and perhaps revisiting the planner behavior
    >> separately if we ever want to refine handling of pruned partitions or
    >> dummy roots. I understand, as was reported upthread, that the EXPLAIN
    >> VERBOSE output isn’t very consistent with that patch even though the
    >> internal error goes away.  Making sense of the output differences
    >> requires knowing that the targetlist population behavior differs
    >> depending on whether enable_partition_pruning is on or off as I
    >> described above.
    >
    > The executor-side fix works for me
    
    Thanks for checking.
    
    > and the test case should be added to your patch.
    > Should we add some comments to explain the output difference in EXPLAIN VERBOSE
    > if enable_partition_pruning is set to a different value?
    
    I added some in the v2 patch I just posted.
    
    -- 
    Thanks, Amit Langote
    
    
    
    
  22. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tender Wang <tndrwang@gmail.com> — 2025-11-07T06:35:47Z

    Amit Langote <amitlangote09@gmail.com> 于2025年11月7日周五 14:04写道:
    
    > Hi,
    >
    > On Fri, Nov 7, 2025 at 10:01 AM Tender Wang <tndrwang@gmail.com> wrote:
    > > Amit Langote <amitlangote09@gmail.com> 于2025年11月6日周四 18:00写道:
    > >> So for back branches, I’d propose sticking with the smaller
    > >> executor-side fix and perhaps revisiting the planner behavior
    > >> separately if we ever want to refine handling of pruned partitions or
    > >> dummy roots. I understand, as was reported upthread, that the EXPLAIN
    > >> VERBOSE output isn’t very consistent with that patch even though the
    > >> internal error goes away.  Making sense of the output differences
    > >> requires knowing that the targetlist population behavior differs
    > >> depending on whether enable_partition_pruning is on or off as I
    > >> described above.
    > >
    > > The executor-side fix works for me
    >
    > Thanks for checking.
    >
    > > and the test case should be added to your patch.
    > > Should we add some comments to explain the output difference in EXPLAIN
    > VERBOSE
    > > if enable_partition_pruning is set to a different value?
    >
    > I added some in the v2 patch I just posted.
    >
    
    I run tests in regress and file_fdw, no failed cases.
    No objections from me.
    
    -- 
    Thanks,
    Tender Wang
    
  23. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Kirill Reshke <reshkekirill@gmail.com> — 2025-11-07T09:04:53Z

    On Fri, 7 Nov 2025 at 11:02, Amit Langote <amitlangote09@gmail.com> wrote:
    >
    > On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > > I looked at a few options, but none seem non-invasive enough for
    > > back-patching, apart from the first patch I posted. That one makes
    > > ExecInitModifyTable() not require a ctid to be present to set the root
    > > partitioned table’s ri_RowIdAttNo, except in the case of MERGE, which
    > > seems to need it. The corner case that triggers the internal error for
    > > UPDATE/DELETE doesn’t occur for MERGE now and likely won’t when
    > > foreign tables eventually gain MERGE support; don't mark my words
    > > though ;-).
    >
    > Well, OK, I just had not tried hard enough to see that the same error
    > happens for MERGE too.
    >
    > With my patch applied:
    > EXPLAIN VERBOSE MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a,
    > b) ON false WHEN MATCHED THEN UPDATE SET b = s.b;
    > ERROR:  could not find junk ctid column
    >
    > I have another idea: we can simply recognize the corner condition that
    > throws this error in ExecInitModifyTable() by checking if
    > ModifyTable.resultRelations contains only the root partitioned table.
    > That can only happen for UPDATE, DELETE, or MERGE when all child
    > relations were excluded.
    >
    > Patch doing that attached.  Added test cases to file_fdw's suite.
    >
    > --
    > Thanks, Amit Langote
    
    
    HI!
    
    I think this is an OK option for backpatching.  After v2 applied, I
    found the behavior of DELETE and EXPLAIN DELETE consistent. The only
    remaining issue is VERBOSE output difference with or without
    enable_partition_pruning (which is v19+ issue to worry about),
    correct?
    
    Also, should we add  COSTS OFF to EXPLAIN in the regression test? I
    understand that costs should be always zero, but COSTS OFF is almost
    everywhere is tests
    
    -- 
    Best regards,
    Kirill Reshke
    
    
    
    
  24. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-07T09:23:16Z

    Hi,
    
    On Fri, Nov 7, 2025 at 6:05 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > On Fri, 7 Nov 2025 at 11:02, Amit Langote <amitlangote09@gmail.com> wrote:
    > >
    > > On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > > > I looked at a few options, but none seem non-invasive enough for
    > > > back-patching, apart from the first patch I posted. That one makes
    > > > ExecInitModifyTable() not require a ctid to be present to set the root
    > > > partitioned table’s ri_RowIdAttNo, except in the case of MERGE, which
    > > > seems to need it. The corner case that triggers the internal error for
    > > > UPDATE/DELETE doesn’t occur for MERGE now and likely won’t when
    > > > foreign tables eventually gain MERGE support; don't mark my words
    > > > though ;-).
    > >
    > > Well, OK, I just had not tried hard enough to see that the same error
    > > happens for MERGE too.
    > >
    > > With my patch applied:
    > > EXPLAIN VERBOSE MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a,
    > > b) ON false WHEN MATCHED THEN UPDATE SET b = s.b;
    > > ERROR:  could not find junk ctid column
    > >
    > > I have another idea: we can simply recognize the corner condition that
    > > throws this error in ExecInitModifyTable() by checking if
    > > ModifyTable.resultRelations contains only the root partitioned table.
    > > That can only happen for UPDATE, DELETE, or MERGE when all child
    > > relations were excluded.
    > >
    > > Patch doing that attached.  Added test cases to file_fdw's suite.
    >
    > HI!
    >
    > I think this is an OK option for backpatching.  After v2 applied, I
    > found the behavior of DELETE and EXPLAIN DELETE consistent.
    
    Thanks for the comment.
    
    > The only
    > remaining issue is VERBOSE output difference with or without
    > enable_partition_pruning (which is v19+ issue to worry about),
    > correct?
    
    Yes, iff we are to do anything at all about the difference.
    
    > Also, should we add  COSTS OFF to EXPLAIN in the regression test? I
    > understand that costs should be always zero, but COSTS OFF is almost
    > everywhere is tests
    
    Yeah, a good call.
    
    v3 attached.
    
    -- 
    Thanks, Amit Langote
    
  25. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-08T05:19:36Z

    On Fri, Nov 7, 2025 at 6:23 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > On Fri, Nov 7, 2025 at 6:05 PM Kirill Reshke <reshkekirill@gmail.com> wrote:
    > > On Fri, 7 Nov 2025 at 11:02, Amit Langote <amitlangote09@gmail.com> wrote:
    > > > I have another idea: we can simply recognize the corner condition that
    > > > throws this error in ExecInitModifyTable() by checking if
    > > > ModifyTable.resultRelations contains only the root partitioned table.
    > > > That can only happen for UPDATE, DELETE, or MERGE when all child
    > > > relations were excluded.
    > > >
    > > > Patch doing that attached.  Added test cases to file_fdw's suite.
    > >
    > > I think this is an OK option for backpatching.  After v2 applied, I
    > > found the behavior of DELETE and EXPLAIN DELETE consistent.
    >
    > Thanks for the comment.
    >
    > > The only
    > > remaining issue is VERBOSE output difference with or without
    > > enable_partition_pruning (which is v19+ issue to worry about),
    > > correct?
    >
    > Yes, iff we are to do anything at all about the difference.
    >
    > > Also, should we add  COSTS OFF to EXPLAIN in the regression test? I
    > > understand that costs should be always zero, but COSTS OFF is almost
    > > everywhere is tests
    >
    > Yeah, a good call.
    >
    > v3 attached.
    
    Attached v4 where I have updated the commit message to mention 86dc9005.
    
    The bug doesn’t seem critical enough to rush the fix, so I’ll hold off
    on committing it for next week’s release to leave room for further
    comments.
    
    -- 
    Thanks, Amit Langote
    
  26. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-26T11:27:04Z

    On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > Among those options, I considered the following block, which adds a
    > ctid for the partitioned root table when it’s the only target in the
    > query after partition pruning removes all child tables due to the
    > WHERE false condition in the problematic case:
    >
    >     /*
    >      * Ordinarily, we expect that leaf result relation(s) will have added some
    >      * ROWID_VAR Vars to the query.  However, it's possible that constraint
    >      * exclusion suppressed every leaf relation.  The executor will get upset
    >      * if the plan has no row identity columns at all, even though it will
    >      * certainly process no rows.  Handle this edge case by re-opening the top
    >      * result relation and adding the row identity columns it would have used,
    >      * as preprocess_targetlist() would have done if it weren't marked "inh".
    >      * Then re-run build_base_rel_tlists() to ensure that the added columns
    >      * get propagated to the relation's reltarget.  (This is a bit ugly, but
    >      * it seems better to confine the ugliness and extra cycles to this
    >      * unusual corner case.)
    >      */
    >     if (root->row_identity_vars == NIL)
    >     {
    >         Relation    target_relation;
    >
    >         target_relation = table_open(target_rte->relid, NoLock);
    >         add_row_identity_columns(root, result_relation,
    >                                  target_rte, target_relation);
    >         table_close(target_relation, NoLock);
    >         build_base_rel_tlists(root, root->processed_tlist);
    >         /* There are no ROWID_VAR Vars in this case, so we're done. */
    >         return;
    >     }
    >
    > If enable_partition_pruning is off, root->row_identity_vars already
    > contains a RowIdentityVarInfo entry for the tableoid Var that was
    > added while processing the foreign-table child partition. Because of
    > that, the if (root->row_identity_vars == NIL) block doesn’t run in
    > this case, so it won’t add any row identity columns such as ctid for
    > the partitioned root table.
    >
    > In theory, we could prevent the planner from adding tableoid in the
    > first place when the child table doesn’t support any row identity
    > column -- or worse, doesn’t support the UPDATE/DELETE/MERGE command at
    > all -- but doing so would require changing the order in which tableoid
    > appears in root->processed_tlist. That would be too invasive for a
    > back-patch.
    
    I’ve implemented this alternative as well -- the version that prevents
    adding tableoid when no other row-identity columns are added for the
    child. That allows to keep root->row_identity_vars empty so the
    dummy-root path can add ctid as intended by the above code block of
    distribute_row_identity_vars().
    
    This provides an alternative approach to compare against the other patch.
    
    -- 
    Thanks, Amit Langote
    
  27. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    amit <amitlangote09@gmail.com> — 2025-11-27T01:25:36Z

    On Wed, Nov 26, 2025 at 8:27 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
    > > Among those options, I considered the following block, which adds a
    > > ctid for the partitioned root table when it’s the only target in the
    > > query after partition pruning removes all child tables due to the
    > > WHERE false condition in the problematic case:
    > >
    > >     /*
    > >      * Ordinarily, we expect that leaf result relation(s) will have added some
    > >      * ROWID_VAR Vars to the query.  However, it's possible that constraint
    > >      * exclusion suppressed every leaf relation.  The executor will get upset
    > >      * if the plan has no row identity columns at all, even though it will
    > >      * certainly process no rows.  Handle this edge case by re-opening the top
    > >      * result relation and adding the row identity columns it would have used,
    > >      * as preprocess_targetlist() would have done if it weren't marked "inh".
    > >      * Then re-run build_base_rel_tlists() to ensure that the added columns
    > >      * get propagated to the relation's reltarget.  (This is a bit ugly, but
    > >      * it seems better to confine the ugliness and extra cycles to this
    > >      * unusual corner case.)
    > >      */
    > >     if (root->row_identity_vars == NIL)
    > >     {
    > >         Relation    target_relation;
    > >
    > >         target_relation = table_open(target_rte->relid, NoLock);
    > >         add_row_identity_columns(root, result_relation,
    > >                                  target_rte, target_relation);
    > >         table_close(target_relation, NoLock);
    > >         build_base_rel_tlists(root, root->processed_tlist);
    > >         /* There are no ROWID_VAR Vars in this case, so we're done. */
    > >         return;
    > >     }
    > >
    > > If enable_partition_pruning is off, root->row_identity_vars already
    > > contains a RowIdentityVarInfo entry for the tableoid Var that was
    > > added while processing the foreign-table child partition. Because of
    > > that, the if (root->row_identity_vars == NIL) block doesn’t run in
    > > this case, so it won’t add any row identity columns such as ctid for
    > > the partitioned root table.
    > >
    > > In theory, we could prevent the planner from adding tableoid in the
    > > first place when the child table doesn’t support any row identity
    > > column -- or worse, doesn’t support the UPDATE/DELETE/MERGE command at
    > > all -- but doing so would require changing the order in which tableoid
    > > appears in root->processed_tlist. That would be too invasive for a
    > > back-patch.
    >
    > I’ve implemented this alternative as well -- the version that prevents
    > adding tableoid when no other row-identity columns are added for the
    > child. That allows to keep root->row_identity_vars empty so the
    > dummy-root path can add ctid as intended by the above code block of
    > distribute_row_identity_vars().
    >
    > This provides an alternative approach to compare against the other patch.
    
    Forgot to mention that with this approach, unlike the other patch, the
    targetlists are identical whether or not enable_partition_pruning is
    on, since in both cases the if (root->row_identity_vars == NIL) block
    of distribute_row_identity_vars() executes:
    
    +SET enable_partition_pruning TO off;
    +EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
    +           QUERY PLAN
    +--------------------------------
    + Delete on public.pt
    +   ->  Result
    +         Output: pt.ctid
    +         Replaces: Scan on pt
    +         One-Time Filter: false
    +(5 rows)
    
    +SET enable_partition_pruning TO on;
    +EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
    +           QUERY PLAN
    +--------------------------------
    + Delete on public.pt
    +   ->  Result
    +         Output: ctid
    +         Replaces: Scan on pt
    +         One-Time Filter: false
    +(5 rows)
    
    pt.ctid in the off case vs only ctid in the on case has to do, I
    think, with there being more than one entry in the rtable in the
    pruning off case.
    
    -- 
    Thanks, Amit Langote
    
    
    
    
  28. Re: BUG #19099: Conditional DELETE from partitioned table with non-updatable partition raises internal error

    Tender Wang <tndrwang@gmail.com> — 2025-11-30T05:59:59Z

    Amit Langote <amitlangote09@gmail.com> 于2025年11月26日周三 19:27写道:
    
    > On Thu, Nov 6, 2025 at 7:00 PM Amit Langote <amitlangote09@gmail.com>
    > wrote:
    > > Among those options, I considered the following block, which adds a
    > > ctid for the partitioned root table when it’s the only target in the
    > > query after partition pruning removes all child tables due to the
    > > WHERE false condition in the problematic case:
    > >
    > >     /*
    > >      * Ordinarily, we expect that leaf result relation(s) will have
    > added some
    > >      * ROWID_VAR Vars to the query.  However, it's possible that
    > constraint
    > >      * exclusion suppressed every leaf relation.  The executor will get
    > upset
    > >      * if the plan has no row identity columns at all, even though it
    > will
    > >      * certainly process no rows.  Handle this edge case by re-opening
    > the top
    > >      * result relation and adding the row identity columns it would have
    > used,
    > >      * as preprocess_targetlist() would have done if it weren't marked
    > "inh".
    > >      * Then re-run build_base_rel_tlists() to ensure that the added
    > columns
    > >      * get propagated to the relation's reltarget.  (This is a bit ugly,
    > but
    > >      * it seems better to confine the ugliness and extra cycles to this
    > >      * unusual corner case.)
    > >      */
    > >     if (root->row_identity_vars == NIL)
    > >     {
    > >         Relation    target_relation;
    > >
    > >         target_relation = table_open(target_rte->relid, NoLock);
    > >         add_row_identity_columns(root, result_relation,
    > >                                  target_rte, target_relation);
    > >         table_close(target_relation, NoLock);
    > >         build_base_rel_tlists(root, root->processed_tlist);
    > >         /* There are no ROWID_VAR Vars in this case, so we're done. */
    > >         return;
    > >     }
    > >
    > > If enable_partition_pruning is off, root->row_identity_vars already
    > > contains a RowIdentityVarInfo entry for the tableoid Var that was
    > > added while processing the foreign-table child partition. Because of
    > > that, the if (root->row_identity_vars == NIL) block doesn’t run in
    > > this case, so it won’t add any row identity columns such as ctid for
    > > the partitioned root table.
    > >
    > > In theory, we could prevent the planner from adding tableoid in the
    > > first place when the child table doesn’t support any row identity
    > > column -- or worse, doesn’t support the UPDATE/DELETE/MERGE command at
    > > all -- but doing so would require changing the order in which tableoid
    > > appears in root->processed_tlist. That would be too invasive for a
    > > back-patch.
    >
    > I’ve implemented this alternative as well -- the version that prevents
    > adding tableoid when no other row-identity columns are added for the
    > child. That allows to keep root->row_identity_vars empty so the
    > dummy-root path can add ctid as intended by the above code block of
    > distribute_row_identity_vars().
    >
    > This provides an alternative approach to compare against the other patch.
    >
    > --
    > Thanks, Amit Langote
    >
    
    I apply the patch, and I find it forgets to update the diff for
    postgres_fdw.
    So I add it in the v2 patch.
    With this patch, the targetlists are identical whether or not
    enable_partition_pruning is on.
    
    In my first email on this thread, to avoid adding "tableoid", I tried to
    add the following codes:
     "(childrte->relkind != RELKIND_PARTITIONED_TABLE && childrte->relkind
    != RELKIND_FOREIGN_TABLE)"
    in expand_single_inheritance_child().
    
    But this didn't work for all test cases. It would trigger an assert failure
    in fix_scan_expr_walker():
    Assert(!(IsA(node, Var) && ((Var *) node)->varno == ROWID_VAR));
    
    Your patch is much better than mine.
    
     --
    Thanks,
    Tender Wang