| 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 |
| 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 |