Re: Unsafe qual pushdown through DISTINCT with simple CASE expressions

From: Richard Guo <guofenglinux(at)gmail(dot)com>
To: Tender Wang <tndrwang(at)gmail(dot)com>
Cc: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: Re: Unsafe qual pushdown through DISTINCT with simple CASE expressions
Date: 2026-08-28 02:20:33
Message-ID: CAMbWs4_6Yvqn8cSxGvYxp4c0yb4k2p-6Mb8_gCSrwYjNKoON=g@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Tue, Aug 18, 2026 at 9:54 AM Tender Wang <tndrwang(at)gmail(dot)com> wrote:
> On master, the qual is pushed below the DISTINCT:
>
> ```text
> Unique
> -> Sort
> Sort Key: cit.t
> -> Seq Scan on cit
> Filter: (CASE (t)::text WHEN 'A'::text THEN 1 ELSE 0 END = 1)
> ```
>
> This is suspicious because DISTINCT compares `t` using citext
> equality, under which `'a'` and `'A'` are equal, while the CASE
> expression casts `t` to text and therefore distinguishes them.

Thanks for the report. I looked into it and I think the root cause is
that grouping_conflict_walker() treats the arg of a simple CASE as an
operand of each WHEN comparison, but only applied the collation half
of the direct-operand check, on the assumption that the WHEN operator
is always the type-default "=" and thus matches the grouping eqop.
That is not true once the arg is relabeled. In your case with "CASE
t::text WHEN 'A'", the WHEN compares with texteq while the grouping
uses citext_eq, so the qual should be rejected just as "t::text = 'A'"
already is.

I reviewed your patch. IIUC, it fixes this by adding an
equality_ops_are_compatible() check per WHEN inside the CaseExpr
branch. That works for the reported query, but I'd rather not go that
way, for a few reasons.

It duplicates the operand check that grouping_check_operand already
implements, so the two would have to be kept in sync.

It assumes each WHEN condition is a bare OpExpr (the Assert), which
the parser does not guarantee. It also skips the
op_is_safe_index_member() gate, which is what makes the opfamily test
meaningful for the direct-operand form.

More generally, the problem is that the CaseExpr branch re-implements
how a direct operand is checked, and does so incompletely.

I think it'd be better to avoid this duplication. So I'd like to take
the approach used elsewhere in planner for the same placeholder: while
walking the WHEN conditions, the walker binds a Var arg in the context
and resolves each CaseTestExpr to it. The Var is then checked as each
WHEN uses it. This is how eval_const_expressions() handles the
CaseTestExpr nodes.

Attached is the patch doing that.

- Richard

Attachment Content-Type Size
v2-0001-Fix-qual-pushdown-past-grouping-through-simple-CA.patch application/octet-stream 10.8 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message shihao zhong 2026-08-28 02:34:51 Re: Return pg_control from pg_backup_stop().
Previous Message David Rowley 2026-08-28 01:15:54 Re: More partition pruning bugs with multi-column RANGE partitions